당니의 개발자 스토리

회원 서비스 개발 본문

스프링/실전! 스프링 부트와 JPA 활용1

회원 서비스 개발

clainy 2024. 4. 24. 22:51

회원 서비스 개발

이번 시간에는 회원 리포지토리에 이어서 회원 서비스를 한번 개발해 보겠습니다.

회원 서비스는 여기다가 service 패키지를 만들 거구요. 그 다음에 MemberService라고 만들 겁니다.

스프링이 또 @Service 라는 어노테이션을 제공하죠.

이걸 쓰면 이 @Service 어노테이션에 또 @Component 라고 되어있기 때문에 컴포넌트 스캔의 대상이 되어서 자동으로 스프링 빈으로 등록이 됩니다.

우선 MemberService는 MemberRepository를 쓸 거기 때문에 방금 저희가 만든 MemberRepository를 이렇게 @Autowired를 쓰면 Injection이 되죠. Injection 하는 방법에는 여러가지가 있는데 우선 지금은 얘를 쓰고 뒤에 더 좋은 방법을 말씀드릴게요.

자 우리가 이번에 개발해야 될 기능이 회원 가입 만들어야 되고, 회원 전체 조회가 있어야 됩니다.

이 두가지가 이전 강의에서 샘플로 화면을 만들었던 거를 생각해보시면 회원 가입하는 게 있었고 가입된 회원 목록을 쫙 뿌리는 게 있었죠. 그래서 이 두가지가 핵심 기능이구요.

자 먼저 회원가입은 join 이라고 해서 이 멤버 객체를 그냥 넘기면 되게 해놓을게요.

얘는 그냥 단순합니다. 우리가 만들었던 memberRepository에 save로 member를 넘기면 끝입니다.

자 그런데 여기서 뭘 해야 되냐면, 우리가 회원 가입할 때 기본적으로 같은 이름인 회원이면 안 된다고, 저희가 중복 회원을 검증한다고 로직을 하나 넣어볼게요.

그러면 validateDuplicateMember(member) 해가지고 이거에 대해서 기본적인 validation(확인, 인증)을 하나 해보겠습니다.

option + enter 해서 메서드를 만들겠습니다.

일단 return 을 완성해놓고 중복 회원이 있는지 여기서 한번 검증하는 로직을 넣을 거에요. 비즈니스 로직을 넣는 거죠.

만약에 중복 회원이면 validateDuplicateMember 메서드에서 예외를 터트려버릴 거예요.

그러니까 문제가 있으면 예외를 터뜨려 버릴 거고, 문제가 없으면 그 다음 로직으로 넘어가서 회원을 정상적으로 save 하고 member.getId()를 해서 id를 반환하죠.

이거는 뭐냐면 딱 이 스타일이 좋은 코딩이 아닌 것 같은데,

우선 이게 jpa에서 em.persist를 하면 이 순간에 뭐가 되냐면 영속성 컨텍스트에 이 member 객체를 딱 올린단 말이에요. 그때 영속성 컨텍스트는 PK, 즉 Key랑 Value가 있는데 Key가 뭐가 되냐면,

이 id 값이 Key가 됩니다. db의 pk랑 매핑한 게 Key가 되거든요. 그래서 이 @GeneratedValue를 세팅을 하면 데이터베이스마다 다른데 어떤 애들은 시퀀스를 넣기도 하고, 어떤 애들은 테이블을 가짜로 만들어서 그 테이블로부터 key를 따서 넣기도 하는데 어쨌든 이 id 값이 항상 생성이 되어있는 게 보장이 됩니다. 그래서 여기 값이 항상 박혀 들어가 있거든요. em.persist 할 때요.

왜냐하면 영속성 컨텍스트에 값을 딱 넣어야 되는데 그때 이제 Key하고 Value가 되잖아요.

그러면 보통 구조가 Key가 뭐냐면 저 PK에 값을 그대로 넣는 거에요.

그러면서 동시에 여기에 값도 채워줍니다. 아직 db에 들어간 시점이 아니어도 그렇게 해준다는 얘기였구요.

자 그래서 이렇게 꺼내면 항상 값이 있다는게 보장이 됩니다. 그렇게 해서 id라도 반환을 해야 뭐가 저장됐는지 알 수가 있겠죠.

그 다음에 여기서 이제 지금 중복 회원을 검증하는 거기 때문에

이렇게 하면 되겠죠. memberRepository의 findByName으로 같은 이름이 있는지를 찾아볼 거에요. 그리고 얘의 반환값이 findMembers란 말이에요. 자 그러면 이제 이름으로 뭔가 조회가 되면 안되겠죠? 이미 가입된 회원이 있다는 뜻이잖아요.

그래서 만약 findMembers.isEmpty()가 아니면 이건 뭔가 잘못된 거죠. 그래서 예외를 날려줍니다. 이렇게 하면 validateDuplicateMember 로직을 완성할 수 있겠죠.

사실 뭐 이렇게까지 안해도 되고, 더 간단하게 그냥 멤버 수를 세 가지고 그냥 그게 수가 0보다 크면 문제가 있다거나 이런 식으로 로직을 하는게 좀 더 최적화되겠죠. 예제에서는 간단하게 여기서 이 정도만 하겠습니다.

이거 기능에 대해서는 다음에 테스트 코드에서 실제 동작하는지 보여드리겠습니다.

그 다음에 이제 전체 회원 조회를 해보겠습니다.

우리가 만들어둔 걸 쓰기만 하면 끝나겠죠. 그러면 회원 전체 조회 기능이 반환이 되고요.

그 다음에 만약에 한 건만 조회하는 일이 있다고 치면, 하나를 조회하는 것도 비즈니스 상 많이 필요하겠죠.

이렇게 하면 단건 조회가 완성됩니다.

자 여기서 이제 기술적인 설명들 하나씩 해드리겠습니다.

먼저 @Service는 아실 거고 여기서 사실 중요한 게 하나 빠졌어요. JPA의 어떤 모든 데이터 변경이나, 어떤 로직들은 가급적이면 트랜잭션 안에서 다 실행돼야 되거든요.

그래서 @Transactional이 있어야 됩니다. 그래야 뭐 LAZY 로딩 이런 게 다 되거든요. 뭐 여기서 Open Session In View 복잡한 내용이 더 있긴 한데 그건 저 뒤에서 따로 설명드릴 거고 기본적으로 트랜잭션 안에서 데이터 변경하는 것은 트랜젝션이 꼭 있어야 해요.

그러면 클래스(MemberService) 레벨의 @Transactional을 쓰면 여기 public method들은 기본적으로 트랜젝션이 걸려 들어갑니다.

그런데 뭐가 있냐면, 우선 @Transactional 어노테이션이 두 개가 있습니다.

Javax가 있고요. Spring에서 제공하는 @Transactional이 있는데 이미 Spring을 쓰고 Spring에 Dependency와 로직이 많이 들어갔기 때문에 Spring이 제공하는 @Transactional 어노테이션을 쓰는 게 더 나아요.

그래야 지금처럼 쓸 수 있는 게 쭉쭉 나오죠. 쓸 수 있는 옵션들이 많아서 이걸 쓰시는 걸 권장드립니다. 그리고 제가 하나 보여드릴게요.

이렇게 해보겠습니다. 이 Transactional에 readOnly = true 라는 옵션을 주시면은,

이게 jpa가 조회하는 곳에서는 조금 더 성능을 최적화 합니다. 디테일하게 들어가면 몇가지가 있는데 역속성 컨텍스트를 flush 안하거나 더티 체킹을 안하는 거로 인한 이점도 있고, 추가로 데이터베이스에 따라서는 readOnly가 true니까 '읽기 전용 트랜잭션이네' 라고 하면 DB한테 '이건 읽기 전용이니까 리소스를 너무 많이 쓰지 말고 단순히 읽기용 모드로 해서 DB야 니가 읽어!' 이렇게 해주는 드라이버들도 있어요.

아무튼 그래서 결론적으로 말씀드리면 읽기에는 가급적이면 readOnly = true를 넣어주시면 되구요. 읽기가 아닌 쓰기에는 무조건 readOnly = true를 넣으시면 절대 안됩니다. 이걸 넣으시면 데이터 변경이 안됩니다.

그런데 지금 보면 여기에는 읽기가 더 많죠?

그러면 맨 위에 @Transactional(readOnly = true)을 넣고 나머지 애들은 그냥 지우는 겁니다. 그리고 중요한 쓰기(join, 등록)에는 transaction을 놔두면 되죠.

기본적으로는 맨 위에 있는 @Transactional(readOnly = true)이 안에 public 메서드에 다 먹히고, 근데 이렇게 따로 @Transactional 설정을 한 거는 이게 우선권을 가져서 @Transactional이 먹히는 거죠. @Transactional은 기본적으로 readOnly가 false거든요.

이런 스타일로 세팅하는 걸 권장드립니다.

근데 막 예를 들어서 MemberService가 정말 command성이어서 정말 거의 쓰기만 있다고 하면

이렇게 하시는 게 더 나은 선택이겠죠.

그리고 @Autowired 하면 Spring이 Spring Bean에 등록되어 있는 MemberRepository를 Injection 해줍니다. 이런 걸 Field Injection 이라고 하죠. 밑에서 다른 더 좋은 Injection에서 설명 드리겠습니다.

그리고 이제 참고로.

여기 보면 validation logic을 보고 개발 많이 하셨던 분들은 '어? 이렇게 해도 문제될 건데' 라고 생각할 거에요.

왜냐면 와스가 동시에 여러개가 뜨잖아요. 그러면 어떤 애가 DB에 똑같은, 예를 들어서 이름이 멤버 A라는 애가 있다고 할게요. 멤버 A라는 이름을 가진 둘이 동시에 DB에 insert를 하게 되면 둘이 동시에 ValidateDuplicateMember를 통과하게 되면서 이 save 로직을 둘 다 동시에 호출할 수 있거든요. 이렇게 되면 정말 동시에 멤버 A라는 이름으로 회원 2명 가입하는 거에요. 그러니까 이제 문제가 될 수 있죠.

그래서 비즈니스 로직이 이렇게 있다고 하더라도 실무에서는 한 번 더 최후의 방어를 해야 되겠죠.

그래서 멀티 쓰레드나 이런 상황을 고려해서 데이터베이스에 Member의 name을 Unique 제약 조건으로 잡아 주시는 것을 권장 드립니다. 그게 더 안전하겠죠.

그리고 이번에는 사실 이제 여러분이 스프링 많이 쓰셨고 어느정도 아신다는 가정 하에 지금 이 강의를 찍고 있는 거긴 한데,

지금처럼 이렇게 많이 쓰시죠. 그런데 이게 단점이 많아요. 어떤 단점이 있냐면 일단 MemberRepository를 못 바꾸잖아요. MemberRepository를 테스트를 하거나 이럴 때 좀 바꿔야 될 때가 있는데 못 바꾸죠. 이미 access 할 수 있는 방법이 없잖아요. 필드고, private 으로 되어있고.

자 그래서 이제 어떤 방법을 쓰냐면,

이렇게 해서 Setter Injection을 또 쓰시거든요. 그래서 스프링이 바로 주입하는 게 아니라 setter 메서드를 통해서 들어와서 주입을 해줍니다. 자 근데 이 방식은 어떤 장점이 있냐면 테스트 코드 같은 거를 작성을 할 때 목 같은 걸 내가 여기 직접 주입해 줄 수가 있어요. 이제 장점이죠. 필드는 그냥 주입하기가 되게 까다로워요.

그런데 Setter Injection은 메서드를 통해서 주입을 하니까 가짜 MemberRepository 같은 걸 주입을 할 수가 있죠.

근데 얘는 단점이 뭐냐면, 치명적인데 이게 정말 런타임에 뭔가 실제 애플리케이션 돌아가는 시점에 누군가가 이거를 바꿀 수 있잖아요.

우리 개발할 때 잘 생각해보시면 정말 setMemberRepository를 호출해서 개발하는 중간에 MemberRepository를 바꿀 일이 있을까요? 없단 말이에요. 보통 애플리케이션 로딩 시점에 조립이 다 끝나버려요. 착착착 해서 MemberService는 이 MemberRepository를 쓴다고 하면서 다 끝나버린단 말이에요.

애플리케이션을 딱 실행해서 스프링이 올라오는 타이밍에 세팅 조립이 다 끝나버려요. 이거를 조립한 이후에 내가 뭔가 실제 애플리케이션 동작을 잘 하고 있는데 바꿀 일이 없단 말이에요. 자 그러면은 이 Setter Injection이 사실 안 좋아요.

그래서 궁극적으로 요즘에 권장하는 방법은 생성자 Injection을 쓰는 겁니다.

자 이렇게 하면 스프링이 뜰 때 생성자에서 MemberRepository를 Injection을 해줘요. 그런데 어쨌든 생성자 인젝션을 쓰면 한번 생성할 때 이게 다 끝나버리기 때문에, 완성이 되기 때문에 중간에 set 해서 이 memberRepository를 바꿀 수 있는 일이 없죠.

그리고 또 좋은 게 테스트 케이스 같은 걸 작성하실 때 이 MemberService를 제가 예를 들어서

이렇게 임의로 테스트를 작성을 하는데 MemberService를 작성을 한단 말이에요. new MemberService()를 하면 여기 지금 빨간불 들어오는 거 있죠. 테스트 케이스에서 주입을 할 때 내가 직접 주입을 해줘야 돼요.

Mock()을 주입을 하든, 이런 걸 안 놓치고 잘 챙길 수 있거든요. 생성 시점에 딱 '얘는 이게 필요해! 이걸 의존하고 있어.'라는 걸 명확하게 알 수가 있죠.

자 이런 장점들이 있는데,

이렇게 하면 코드가 좀 번거롭잖아요. 요즘에 좀 좋아진게 뭐냐면,

이렇게 해도 됩니다. 스프링이 요즘 대부분 최신버전을 쓰니까 최신버전 스프링에서는 이렇게 생성자가 딱 하나만 있는 경우에는 스프링이 생성자의 매개변수에다가 @Autowired 어노테이션이 없어도 자동으로 인젝션을 해줘요. 그래서 되게 편하죠.

자 그리고 이제 memberRepository를 변경할 일이 없기 때문에 여기에다가 이 필드를 final로 하는 것을 권장을 해드립니다. final을 하면,

이렇게 했을 때 빨간불이 들어오죠. 생성자를 기껏 만들었는데 값 세팅을 안하면 빨간불 들어오죠. 이렇게 컴파일 시점에 체크를 할 수 있기 때문에 final을 넣으시는 것을 추천을 드려요.

그리고 여기에다가 이제 롬복을 적용을 하면,

Lombok에 뭐가 있냐면 All Arguments Constructor 라는 게 있습니다. @AllArgsConstructor는 뭐냐면,

모든 필드를 가지고 이 생성자와 똑같은 걸 만들어 주는 거에요.

그래서 이렇게 생성자를 지워도 되는거죠.

그런데 이제 @AllArgsConstructor보다 조금 더 나은 게 뭐냐면,

@RequiredArgsConstructor 라는 게 있어요. 얘는 뭐냐면 이 final이 있는 필드만 가지고 생성자를 만들어줘요. 그래서 @RequiredArgsConstructor를 쓰면, 기본적으로 인젝션 하면서 생성자에서 세팅하고 끝날 애들은 final로 잡고 나머지는 중간에 혹시 필드 변경이 필요할 때가 있겠죠. 그래서 @RequiredArgsConstructor를 쓰시는게 조금 더 나은 방법이 됩니다.

더 디테일한 내용은 롬복을 참고해주시구요.

그래서 저는 개인적으로 요즘에 이 스타일을 선호합니다. 이렇게 해놓으면 테스트 케이스에서도 여러 개 실수로 memberRepository를 초기화 안하면 에러를 내주고 하기 때문에 이 방법을 권장해 드립니다. 자 그래서 앞으로 이 스타일로 다 쓸겁니다.

근데 재밌는게 있어요.

우리 만들었던 MemberRepository 있죠. 얘도 생성자 인젝션을 할 수가 있습니다. 스프링이 이렇게 해주거든요. 여러분이 스프링 부트 라이브러리를 쓰시면 그 스프링 부트에 스프링 데이터 jpa를 쓰면 뭘 할 수 있냐면,

@PersistenceContext를 @Autowired로 바꿀 수 있습니다. 이거는 스프링 데이터 jpa가 지원을 해줘서 되는 거고요.

그러면 이제 이걸 할 수가 있는 거죠. 코드를 이렇게 줄일 수가 있습니다. 제가 이제 인젝션에서 쭉 설명을 드렸습니다.

자 그래서 MemberService에 대한 최종 코드가 이렇게 완성이 되는 거죠. 여기까지 설명 드리면 될 것 같구요.

다시 한번 강조드리지만,

이렇게 하면 MemberRepository에 엔티티 매니저를 생성자로 인젝션 한 거거든요.

사실 지금 이 코드란 말이에요. 사실 지금 이게 되려면,

참고로 엔티티 매니저는 @Autowired로 안되고 방금 말씀드린 @PersistenceContext라는 애로 표준 어노테이션이 있어야 인젝션이 됩니다. 근데 스프링 데이터 JPA가 @Autowired로도 인젝션이 되게 지원을 해줘요.

그래서 이게 되는 겁니다. 물론 스프링 데이터 JPA가 없으면,

이게 안 되죠. 근데 제가 알기로는 나중 버전에 지금은 안되는 걸로 아는데, 향후에는 스프링 기본 라이브러리에서도 이게 되도록 @Autowired로 인젝션 할 수 있도록 지원해 줄 예정이라고 합니다.

 

자 여기까지 봤구요.

다음 시간에는 이제까지 만든 기능을 한번 테스트 코드를 작성을 해서 완성을 해보겠습니다.