Today's Codekata
# Consecutive Numbers
SELECT DISTINCT l1.num AS ConsecutiveNums
FROM Logs l1
JOIN Logs l2 ON l1.id = l2.id + 1
JOIN Logs l3 ON l1.id = l3.id + 2
WHERE l1.num = l2.num AND l2.num = l3.num;
# Product Price at a Given Date
SELECT p.product_id,
COALESCE((
SELECT new_price
FROM Products
WHERE product_id = p.product_id
AND change_date <= '2019-08-16'
ORDER BY change_date DESC
LIMIT 1
), 10) AS price
FROM (
SELECT DISTINCT product_id FROM Products
) p;
# Last Person to Fit in the Bus
SELECT q.person_name
FROM (
SELECT person_name, turn,
SUM(weight) OVER (ORDER BY turn) AS total_weight
FROM Queue
) q
WHERE total_weight <= 1000
ORDER BY turn DESC
LIMIT 1;
// 카펫
class Solution {
public int[] solution(int brown, int yellow) {
int[] answer = new int[2];
int n = brown + yellow;
for (int height = 1; height <= n; height++) {
if (n % height == 0) {
int width = n / height;
if ((width - 2) * (height - 2) == yellow) {
answer[0] = width;
answer[1] = height;
break;
}
}
}
return answer;
}
}
Today I Learned
#Bean 생명주기
Spring Bean 생명주기란, Spring이 Bean을 생성하고 관리하고 소멸시키는 전체 흐름을 말한다.
개발자가 직접 객체를 만들고 없애는 게 아니라, Spring이 알아서 해주고 그 과정에서 특정 시점에 콜백 메서드를 실행할 수 있다.
| 단계 | 설명 |
| 1. Spring 컨테이너 생성 | `ApplicationContext` 또는 `BeanFactory` 가 생성되고 설정 정보를 읽음 |
| 2. Bean 인스턴스 생성 | 기본 생성자를 통해 객체 생성 (싱글톤은 앱 시작 시, 프로토타입은 요청 시) |
| 3. 의존성 주입 (DI) | 생성자, setter, 필드 등을 통해 필요한 의존성 주입 |
| 4. 초기화 콜백 | `@PostConstruct`, `afterPropertiesSet()` 등으로 초기화 작업 수행 |
| 5. Bean 사용 | 비즈니스 로직 수행에 Bean 사용 |
| 6. 소멸 콜백 | `@PreDestroy`, `destroy()` 등으로 리소스 정리 |
| 7. Bean 소멸 | 메모리에서 제거됨 |
생명주기 콜백 방식
| 방식 | 초기화 콜백 | 소멸 콜백 | 특징 |
| 인터페이스 | `afterPropertiesSet()` (`InitializingBean`) |
`destroy()` (`DisposableBean`) |
Spring에 종속적이며 명확한 구조 |
| @Bean 속성 | `@Bean(initMethod = "메서드명")` | `@Bean(destroyMethod = "메서드명")` | 메서드 이름 자유 지정, 외부 라이브러리에도 적용 가능 |
| 표준 Annotation | `@PostConstruct` | `@PreDestroy` | Java 표준, 최신 Spring에서 권장, 외부 라이브러리에는 적용 불가 |
#API 예외처리
@ExceptionHandler
- 특정 컨트롤러 클래스 내에서 발생한 예외를 처리하는 메서드에 붙이는 Annotation
- 해당 컨트롤러 클래스 내부에서만 동작한다.
- 예외별로 메서드를 정의해 처리가 가능하다.
- 단일 책임 원칙 위반, 중복 코드 발생, 재사용 어려움 등의 단점이 있다.
@ControllerAdvice
- 모든 컨트롤러에 전역적으로 예외처리를 적용할 수 있는 클래스에 붙이는 Annotation
- 애플리케이션 전체 컨트롤러를 대상으로 한다.
- 공통 예외 처리 로직을 분리하여 재사용성과 유지보수성이 향상된다.
- 뷰 이름, 문자열 등을 반환한다. (일반 MVC 컨트롤러에 적합)
@RestControllerAdvice
- `@ControllerAdvice` + `@ResponseBody` → REST API 예외를 JSON 형태로 처리
- 모든 REST 컨트롤러를 대상으로 한다.
- API 응답을 JSON으로 자동 변환, RESTful 서비스에 최적화 되어있다.
- `ResponseEntity`, JSON 객체 등을 반환한다.
#JPQL
JPQL(Java Persistence Query Language)
- JPA에서 사용하는 객체지향 쿼리 언어로, SQL과 유사하지만 Entity 객체와 필드를 대상으로 질의를 수행한다.
- 다양한 데이터베이스에서 동일한 JPQL 사용 가능하다. (DB 독립성 확보)
- `EntityManager.createQuery("JPQL문", Entity.class)`로 실행하고, `:파라미터명`으로 값을 바인딩한다.
- 정적 쿼리에 최적화, 동적 쿼리는 QueryDSL이 더 적합하다.
JPQL 문법
기본 문법 규칙
- 테이블 이름이 아닌 Entity 이름을 사용하고, 별칭(alias)은 필수다. (단, as는 생략 가능)
- 필드와 Entity는 대소문자 구분, JPQL 키워드는 구분하지 않는다.
- SELECT, UPDATE, DELETE, GROUP BY, ORDER BY, HAVING 등 SQL과 유사한 문법을 사용한다.
SELECT p FROM Product p WHERE p.price > 1000
| 반환 타입 | 설명 | 특징 |
| `TypedQuery` | 반환 타입 명확할 때 | 타입 안정성 높음, 컴파일 시 검증 가능 |
| `Query` | 반환 타입 불명확할 때 | 형 변환 필요, 유연하지만 안전성 낮음 |
- `getResultList()` → 여러 개 결과 조회 (빈 리스트 가능)
- `getSingleResult()` → 하나의 결과 조회 (없거나 여러 개면 예외 발생)
파라미터 바인딩
- 이름 기반 바인딩 권장(`:name`)하고, 순서 기반 바인딩은 유지보수가 어려워서 사용을 지양한다.
em.createQuery("select t from Tutor t where t.name = :name", Tutor.class)
.setParameter("name", "wonuk")
.getSingleResult();
Embedded 타입
- `@Embeddable`, `@Embedded`로 정의
- 여러 Entity에서 재사용 가능, 응집도 높고 단일 책임 원칙(SRP) 준수
프로젝션 (필드 선택 조회)
- Entity 전체, Embedded 타입, 단일 필드(Scala) 모두 조회 가능
- `DISTINCT`로 중복 제거 가능
- `JOIN`을 통해 연관 Entity 조회 가능 (묵시적 JOIN보다 명시적 JOIN 추천)
페이징 처리
em.createQuery("select t from Tutor t order by t.age desc", Tutor.class)
.setFirstResult(5) // 시작 위치
.setMaxResults(10) // 조회 개수 제한
.getResultList();
조건문 & 함수
- 조건 처리: `CASE`, `COALESCE`, `NULLIF`
- 문자열 함수: `CONCAT`, `SUBSTRING`, `TRIM`, `UPPER`, `LOWER`, `LENGTH`
- 숫자 함수: `ABS`, `MOD`, `SIZE`
#Fetch Join
Fetch Join이란?
- 연관된 엔티티를 한 번의 SQL로 함께 조회하는 JPQL 기능이다.
- SQL의 JOIN과 다르게 객체 그래프를 즉시 로딩한다.
- 지연 로딩(LAZY) 설정 무시하고 즉시 로딩(EAGER) 처리된다.
Entity Fetch Join (N:1)
- `Tutor` → `Company` 관계에서 튜터 조회 시 회사도 함께 조회한다.
- `Proxy` 객체가 아닌 실제 `Company` Entity가 영속성 컨텍스트에 등록된다.
Collection Fetch Join (1:N)
- `Company` → `Tutor` 관계에서 회사 조회 시 튜터 리스트도 함께 조회한다.
- 데이터 중복 발생 가능 → `DISTINCT` 사용 권장 (JPQL의 `DISTINCT`는 PK 기준으로 중복을 제거한다.)
- 페이징 불가 → SQL의 `LIMIT`이 적용되지 않음, 결과를 메모리에서 잘라내므로 성능 이슈가 발생할 수 있다.
@BatchSize로 페이징 보완
- `fetch join` 대신 `@BatchSize(size = ...)`로 지연 로딩 최적화가 가능하다.
- `(size = 100)`을 설정하면 하나씩 쿼리를 가져오지 않고, 최대 100개씩 묶어서 한 번에 가져오는 방식이다.
- 한 번에 여러 엔티티를 `IN` 쿼리로 조회한다.
- 전역 설정도 가능하다 (`hibernate.jdbc.batch_size`)
#마치며
오늘은 단순히 문법을 외운 게 아니라, 왜 이런 구조가 필요한지, 어떤 상황에서 쓰이는지를 직접 느낀 하루였다. "이건 왜 이렇게 쓰지?"라는 질문이 "아, 이래서 이렇게 쓰는구나!"로 바뀌어가는 중이다. 조금씩 성장하고 있다는 게 느껴진다. 특히 지난 프로젝트에서 사용했던 기술들을 다시 공부하면서, 그때는 몰랐던 이유와 의도를 이해하게 되었고, 앞으로 어떻게 더 잘 활용할 수 있을지도 고민해보는 시간이 되었다.