프로젝트를 진행하면서 한 페이지에 적기는 짧은 고민들을 담고 있습니다.
그래 전역에서 예외 처리하는거 @ControllerAdvice, @RestControllerAdvice쓰는거 알겠어. 그럼 넌 뭘 예외처리 할건데?
회사에서는 정해진 규칙에 따라서 그냥 아무생각없이 처리했는데 바닥부터 내가 만드니 예외처리에 대해 다시 생각하게 되었다.
2가지 방법이 있다.
status VS throw exception을 찾아보았다.
stackoverflow의 질문
stackexchange의 질문
다양한 의견이 있었고
내가 내린 결론은 난 둘 다!
비즈니스 로직 중 예상 가능한 시나리오에 있다면 status
처리
비즈니스 로직 중 개발자가 알아야 할 상황이라면 throw exepton
처리
어찌보면 당연한 소리지만 본인만의 기준이 없어 이상하게 많이 고민한 문제였다.
현재는 간단하게 세션 토큰을 발급해서 레디스에 저장하고 있다.(session 구현 -> 스프링시큐리티로 리팩토링 예정)
분산환경에서 이 세션 토큰을 관리하는 방법도 있다. Spring Session, Redis를 사용하는 모양이다. 시간이 되면 이쪽 방향으로 리팩토링 해보면 좋을 것 같다.
분산 환경에서 Spring Session과 Redis를 활용하여 세션 토큰 관리
rds와 redis 각각의 멀티 모듈을 만들어 도메인 로직에 필요한 기본적인 repository를 이곳에 위치시켰다.
api서버를 작동시키는 중 문제가 발생했다. 바로 repository의 의존성 주입이 안된다는 문제!
이유
멀티모듈에서 다른 모듈의 의존성을 추가하고 프로젝트를 빌드한다고 해보자.
api 모듈
은 com.api 이하의 패키지에 소스들이 위치하고
domain 모듈
은 com.rds 이하의 패키지에 소스들이 위치한다.
각각 모듈은 빌드가 되고 api 모듈에 domain 모듈의 소스가 위치한 클래스 패스가 추가된다.
스프링부트는 @SpringBootApplication
을 기준, 해당 어노테이션이 있는 위치를 기준으로 Component Scan을 동작한다.
이 때 @SpringBootApplication는 api에 위치하게 되고 빌드하게 될 때 의존성을 추가한 rds는 Component Scan이 이루어지지 못하게된다.
해결방법
의존성 추가된 모듈도 스캔의 대상이 될 수 있게 패키지 스캔의 시작을 그 상위 패키지로 지정하였다.
const val BASE_PACKAGE = "com"
@SpringBootApplication(scanBasePackages = [BASE_PACKAGE])
향로님의 블로그를 보고 로컬환경에서도 바로 실행가능하게 embedded redis를 추가하고 싶었으나....
라이브러리가 맥 실리콘 칩을 지원하지 않았다. 깃 이슈에 가보면 해당 문제를 고친 머지리퀘스트가 존재하지만 어째서인지 여기도 업데이트가 안되고 있다.
방법이 없었던 건 아니였다. 레디스를 로컬에 설치하고 바이너리 파일은 소스에 넣어주고 이 파일을 사용하게 소스코드를 수정해주면 되긴 했지만 내 로컬에서 잘 안됐다. 일단은 갈 길이 구만리라 기록만하고 시간 여유가 있을 때 다시 해보자!
향로님 embedded redis
embedded redis git issue
m1 해결 방법
현재의 상황
쿠키 만료: 30일
토큰 만료: 30일
쿠키의 만료의 따라 토큰 만료도 30일로 지정하였다. 근데 가만 생각하면 MAU가 천만명일경우 레디스에서 천만개의 토큰을 30일 동안 가지고 있어야한다. 뭐 물론 로그아웃하면 중간에 삭제되긴하지만 누가 그렇게 로그아웃을 성실히 누를까
예전 영한샘 수업들은걸 다시 보니 스프링에서 세션을 사용할 수 있게 만들어 놓은 기능이 있었다. 여기서 토큰의 만료시간은 30분이였고 마지막 접속을 기준으로 만료시간을 다시 30분으로 세팅해주었다. 토큰의 만료시간도 아무렇게나 정하는 것이 아니라 시스템에 어떤 영향을 미칠지 주의해야한다.
엔티티
의 연관관계
를 설정하는데 고민이 생겼다. 실무에서는 엔티티가 다른 엔티티에 의해 만들어져서 엔티티의 추적과 테스트 코드의 작성이 매우 힘들었었다.
마침 예전에 읽었던 DDD를 보니 애그리거트는 최대한 쪼개고 ID를 참조하라는 말이 있었다. 또한 더 찾아보니 나와 같은 고민을 한 사람이 있었고 그에 대한 영한샘의 답변이다. 절대적으로 맞는 것은 없고 현재의 나의 기술 이해도, 프로젝트의 상황을 보고 판단하여 적절하게 소스 코드에 녹여내라는 말씀이였다.
1) 애그리거트를 나누실때 어떤 기준으로 나누시는지 궁금합니다 !!
-> 우선 애그리거트는 우리가 생각하는 것 보다 훨씬 작은 단위로 만들어집니다. 애그리거트는 상황마다 다르기는 하지만, 엔티티 하나가 거의 하나의 애그리거트로 만들어집니다. 이정도로 작은 단위로 만들어야합니다.
아이템, 주문, 회원, 결제 이런식으로 도메인을 나누면 이것은 애그리거트보다 상위 개념인 BoundedContext 개념으로 나누어야 합니다.
2) 애그리거트 루트를 참조할때는 객체를 통해 접근하면 각 애그리거트간의 결합도가 상승할거같은데 어떤식으로 처리하시나요 ??
-> DDD에서는 주로 이런 경우 연관관계를 맺지 말고, 식별자로 분리하라고 설명합니다. 이렇게 하면 다른 프로젝트여도 해당 식별자(String id, Long Id 등등)만 가지고 있으면 되기 때문에, 문제가 없습니다.
그런데, 이 모든 것은 프로젝트의 규모에 따른 선택이 필요합니다. 애그러거트 개념을 통해서 애그리거트 단위로 트랜잭션을 만들고 전파하고, 이런 부분은 다른 방면의 복잡성을 매우 높입니다.
추가로 연관관계를 매핑할 때 참조값 대신에 식별자를 사용하는 방법 등은, 모두 좋아보이지만, 실무에서는 이렇게 하면 fetch join을 통한 성능 최적화가 어렵습니다.
DDD를 적용할 때는 모든 것을 적용한다기 보다는, DDD를 재대로 공부하고, 필요한 부분을 우리 프로젝트에 맞게 선택해서 가져오는 것이 맞는 방향이라 생각합니다.
JPA나 ORM을 처음 사용할 때는, 단순히 애그리거트 개념도 빼고, 도메인 이벤트 개념도 빼고, 연관관계는 모두 설정하는 방향으로 기본을 가져가는 것을 저는 권장합니다.
이후에 프로젝트가 커지면 점진적으로 이러한 개념을 도입하고 적용하는게 저는 더 맞다고 생각합니다^^
감사합니다.
답변 참고
키, 밸류 값
을 어떤 자료형을 쓸지 고민이 되었다.
-> 현재 나의 상황은 카프카를 로컬에서만 실행하며 다른 모니터링 시스템을 구축하지 않는다. 따라서 kafka 쉘 스크립트
를 통해 모니터링 해야하는 상황이므로 deserializer
가 필요 없는 String
을 선택하였다.
컨슈머 그룹 아이디
는 무슨 기준으로 만들어야 하지? 컨슈머 그룹 아이디로 뭘 구분해서 어떤 이점을 얻는거지?의 의문들이 생겼다.
공식 문서에서는 다음 3가지와 컨슈머 그룹 아이디가 관련이 있다고 했다.
나의 경우는 포인트와 추가, 적립, 차감의 로직은 같은 컨슈머 그룹으로 묶었고, 차후 포인트와 관련된 알림 전송 기능이 들어간다면 알림 전송 서버
와 또 다른 컨슈머 그룹 아이디
를 만들어, 같은 토픽에 대해 각각 다른 로직을 수행하게 처리할 수 있겠다.
도메인 모듈과 API 모듈이 존재하고 있다. 도메인 모듈에는 entity와 repository가 존재하고 API는 도메인 모듈을 의존한다.
API모듈에서 @DataJpaTest를 통해 querydsld을 하고자했다. 하지만 도메인 모듈의 repository가 의존주입이 안되는 문제가 발생했다. @SpringBootTest로 테스트 실행하면 repository의 의존주입이 또 가능하다.
테스트 코드의 경우 Application 클래스의 설정을 기준으로 빈을 스캔한다고 했다. 아마 이 때 @DataJpaTest는 다른 모듈의 JPA관련 빈들을 스캔하지 못하는 것 같다(추정) 하루를 꼬박 찾았으니 심증만 있고 물증만 없는 상태다. 시간의 여유가 있을 때 이유를 찾아봐야겠다.
JPA에서 연관관계를 맺을 때 entity객체에 쓴 join column과 실제 디비의 외래키가 달라 계층형 쿼리가 계층 끝까지 나가는 문제가 있었다. 컬럼을 찾지 못해 추가적으로 쿼리가 나가는 것 같으니 조심하자.(DB는 스네이크, 소스의 entity는 카멜 표기로 되어있었다.)
코틀린의 경우 클래스가 기본 final로 만들어진다. 프록시 기술을 베이스로 하는 fetch lazy의 경우 엔티티 클래스들을 open으로 다 상속에 열려있게 해주어야한다. 하지만 모든 엔티티에 open을 붙이는 건 개발자의 실수할 여지가 있고 상당히 귀찮은 일이다. 이를 위해 allopen이라는 플러그인이 있으니 원하는 클래스가 open될 수 있도록 설정하자.
plugins {
kotlin("plugin.allopen")
...
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.Embeddable")
annotation("jakarta.persistence.MappedSuperclass")
}
@OneToOne 양방향 연관 관계에서 연관 관계의 주인이 아닌 쪽 엔티티를 조회할 때, Lazy로 동작할 수 없다.
참고