2023-06-23-TIL
2023-06-23-TIL
Today I Learned
Redis Sentinel
Redis Sentinel은 마스터-복제본 구성을 감시하다가 마스터가 응답하지 않으면 복제본 하나를 새 마스터로 승격하는 고가용성 장치다. 문서는 Sentinel의 역할로 모니터링, 알림, 자동 장애 조치, 클라이언트에 현재 마스터 주소를 알려 주는 설정 제공자 역할을 든다.
장애 판정에는 두 단계가 있다. 설정한 quorum 수만큼의 Sentinel이 마스터에 닿지 않는다고 동의해야 마스터를 실패로 표시하고, 실제 장애 조치는 Sentinel 과반수의 투표로 리더가 뽑혀야 시작된다. 문서가 독립적으로 장애가 나는 머신에 최소 세 개의 Sentinel을 두라고 권하는 이유가 이것이다. 두 개만 두면 하나가 죽었을 때 과반수를 만들 수 없다. 복제가 비동기이므로 장애 조치 과정에서 마지막 쓰기 일부를 잃을 수 있다는 점도 문서가 명시한다.
- https://redis.io/docs/management/sentinel/
MapStruct로 엔티티 수정하기
수정 요청 DTO의 값을 기존 엔티티에 덮어쓸 때는 새 객체를 만드는 매핑이 아니라 기존 객체를 갱신하는 매핑이 필요하다. MapStruct는 대상 파라미터에 @MappingTarget을 붙이면 새 인스턴스를 만들지 않고 그 객체의 필드를 갱신한다. 부분 수정(PATCH)이라 DTO의 null 필드는 무시하고 싶다면 nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE를 지정한다. 다만 setter로 엔티티를 바꾸게 되므로, 도메인 규칙을 엔티티 메서드에 담는 설계와는 충돌할 수 있다는 점을 고려해야 한다.
1
2
3
4
5
@Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface ProductMapper {
void update(ProductUpdateRequest request, @MappingTarget Product product);
}
- https://ryumodrn.tistory.com/22
카테고리마다 속성이 다른 상품 DB 설계
카테고리마다 필요한 속성이 다를 때(의류는 사이즈와 색상, 노트북은 CPU와 메모리) 테이블을 어떻게 설계할지의 문제다. 대표적인 선택지는 다음과 같다.
- 카테고리별 테이블: 공통 컬럼은 상품 테이블에, 카테고리 고유 속성은 별도 테이블에 둔다. 타입과 제약을 정확히 걸 수 있지만 카테고리가 늘 때마다 스키마가 바뀐다.
- EAV(Entity-Attribute-Value): 속성 정의 테이블과 상품별 속성 값 테이블을 둔다. 스키마 변경 없이 속성을 추가할 수 있지만 값의 타입 검증이 어렵고 조회 쿼리가 복잡해진다.
- JSON 컬럼: 고유 속성을 JSON으로 저장한다. 유연하지만 검색 조건으로 쓰려면 생성 컬럼이나 함수 인덱스 같은 추가 장치가 필요하다.
링크한 질문의 답변은 속성마다 테이블을 두는 모델을 제안하면서도, 결국 어떤 검색을 주로 하느냐에 따라 달라진다고 덧붙인다.
- https://stackoverflow.com/questions/36164338/design-database-for-multi-category-attribute
QueryDSL의 서브쿼리
QueryDSL JPA에서는 JPAExpressions로 서브쿼리를 만든다. WHERE 절의 in, exists, 비교 조건과 SELECT 절의 스칼라 서브쿼리에 쓸 수 있다. 다만 JPQL 기반이라 FROM 절 서브쿼리(인라인 뷰)는 지원하지 않는다. 이동욱 님의 글은 이런 경우 서브쿼리 대신 조인을 쓰거나, 애플리케이션에서 처리하거나, 쿼리를 나눠 실행하는 방안을 먼저 검토하라고 권한다.
1
2
3
4
List<Member> result = queryFactory.selectFrom(member)
.where(member.age.eq(
JPAExpressions.select(sub.age.max()).from(sub)))
.fetch();
- https://jojoldu.tistory.com/379
- https://stackoverflow.com/questions/29062959/how-to-write-subquery-in-querydsl
JPA @EntityGraph
@EntityGraph는 쿼리마다 함께 조회할 연관관계를 지정하는 JPA 2.1 기능이다. Spring Data JPA에서는 리포지토리 메서드에 @EntityGraph(attributePaths = {"members"})처럼 붙이면 JPQL을 직접 쓰지 않고도 fetch join과 같은 효과를 얻는다. 메서드 이름 쿼리와 함께 쓸 수 있어 간단한 N+1 해결에 편하다. 다만 생성되는 SQL이 LEFT OUTER JOIN이라는 점, 복잡한 조건이 필요하면 결국 JPQL의 join fetch가 더 명확하다는 점을 알아 둔다.
- https://itmoon.tistory.com/77
- Query Methods (Spring Data JPA)
Fetch Join
fetch join은 JPQL에서 join fetch로 연관 엔티티를 같은 SQL에서 함께 조회해 영속성 컨텍스트에 채워 넣는 기능이다. 지연 로딩으로 설정한 연관관계도 해당 쿼리에서만 즉시 가져오므로 N+1 문제의 기본 해결책이다. 컬렉션을 fetch join하면 페이징이 메모리에서 이루어진다는 경고가 나오고, 둘 이상의 컬렉션을 동시에 fetch join하면 MultipleBagFetchException이 날 수 있다는 점이 대표적인 제약이다.
- https://kihwan95.tistory.com/12
JPA getOne()과 findOne()
getOne()(이후 getById(), 현재는 getReferenceById())은 SQL 없이 프록시를 반환하고 실제 조회는 속성에 접근할 때 일어난다. 대상이 없으면 그때 EntityNotFoundException이 난다. findOne()과 findById()는 즉시 조회해 결과를 돌려준다(Spring Data 2.0부터 findById()는 Optional). 연관관계를 설정할 때 ID만 필요하면 프록시로 충분하므로 getReferenceById()가 SELECT를 줄여 준다.
- https://circlee7.medium.com/jpa-getone-id-findone-id-aa9676dc666d
@Transactional self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime
하나의 서비스 클래스에서 트랜잭션 메서드를 실행하면 self-invocation 관련 에러가 발생한다. 따라서 위로 끌어올려서 추상화한 클래스를 따로 두던지, 레파지토리에 의존하여 직접 끌어다 써야한다.
제목의 문장은 Spring 문서의 경고 그대로다. Spring의 선언적 트랜잭션은 프록시 기반이라, 외부에서 프록시를 거쳐 들어온 호출에만 트랜잭션 어드바이스가 적용된다. 같은 객체 안에서 this.otherMethod()로 부르면 프록시를 거치지 않으므로 otherMethod의 @Transactional은 무시된다. IDE가 경고로 알려 주는 것도 이 때문이다. 해결은 메모처럼 트랜잭션 메서드를 다른 빈으로 분리하는 것이 가장 명확하다. 자기 자신의 프록시를 주입받는 방법이나 AspectJ 모드도 있지만 구조를 복잡하게 만든다.
- https://stackoverflow.com/questions/23931698/spring-transactional-annotation-self-invocation
- https://yeonyeon.tistory.com/283
- Using @Transactional (Spring Framework Reference)
Spring Boot와 JWT
JWT 기반 인증은 로그인에 성공하면 서버가 서명한 토큰을 발급하고, 이후 요청은 Authorization: Bearer <token> 헤더로 토큰을 보내는 방식이다. Spring Security에서는 요청마다 토큰을 꺼내 서명과 만료를 검증한 뒤 SecurityContextHolder에 인증 정보를 넣는 필터를 UsernamePasswordAuthenticationFilter 앞에 등록하는 구성이 일반적이다. 세션을 쓰지 않으므로 SessionCreationPolicy.STATELESS로 설정한다. 직접 필터를 만드는 대신 Spring Security의 OAuth2 Resource Server 지원(oauth2ResourceServer().jwt())을 쓰면 검증 로직을 표준 구현에 맡길 수 있다.
- https://www.youtube.com/watch?v=KxqlJblhzfI
- https://www.javainuse.com/spring/boot-jwt
- https://www.toptal.com/spring/spring-security-tutorial
failed to lazily initialize a collection of role
@Transactional을 안 붙여주어서 에러가 발생했다.
LazyInitializationException은 지연 로딩 프록시를 영속성 컨텍스트가 닫힌 뒤에 초기화하려 할 때 발생한다. 트랜잭션 없이 엔티티를 조회하면 조회가 끝나는 즉시 세션이 닫히고, 이후 컬렉션에 접근하는 순간 초기화할 세션이 없다. 그래서 메모처럼 조회부터 컬렉션 접근까지를 하나의 @Transactional 범위로 묶으면 해결된다. 트랜잭션 밖(컨트롤러나 뷰)에서 접근해야 한다면 서비스에서 fetch join이나 @EntityGraph로 미리 가져와 DTO로 변환해 넘기는 것이 근본적인 방법이다.
- https://www.inflearn.com/questions/33949/failed-to-lazily-initialize-a-collection-of-role-%EC%98%A4%EB%A5%98-%EA%B4%80%EB%A0%A8-%EB%AC%B8%EC%9D%98
- https://jungguji.github.io/2021/01/22/failed-to-lazily-initialize-a-collection-of-role/
댓글
아직 댓글이 없습니다