우선 해야할 것이 크게 코드 의존성 관리를 하며 구조를 변경하는 것과 쿼리문을 수정하고 성능을 향상하기위한 2가지 작업을 할 것이다. 오늘은 의존성 관리를 하며 구조를 변경하겠다.
첫 번째로 application계층과 service계층의 역할이 명확하게 분리되지 않았다. 현재 내 비즈니스의 구조는 controller → application → service → repository와 같은 흐름인데 application과 service가 중복되는 작업을 하고 있었다. 이를 명확하게 구분하기 위해 service에 비즈니스를 처리하는 로직을 넣어 책임을 명확하게 할 것이다. 그럼 application계층은 왜 필요하지? 라는 생각이 들지만 application은 service들을 여러개 호출하는 작업에 쓰이는 것이다.
즉 service단은 한 트랜잭션 내에서 발생하는 작업들이 존재하고 application은 각각의 트랜잭션 단위의 작업들의 모음인 것이다.
예를 들어 회원가입을 하면 회원가입을 축하한다는 메일을 보내는 작업이 있을 때 회원 가입을 하며 기본 정보, 상세 정보를 저장하는 이 과정은 한 트랜잭션 내에서 일어나야한다. 하지만 회원가입을하며 기본 정보, 상세 정보가 저장되어도 메일 발송이 되지 않았다면? 이건 상관없는 것이다. 메일을 보낼 때 문제가 생겨도 회원가입한 것을 롤백해야하나? 라는 물음에서 그러면 안된다고 생각한다. 이후 메일이 발송되지 않은 것은 로그를 확인하고 추후에 발송을 해도 되는 것이다. 그래서 application,service모두 repository를 아는 구조에서 repository는 service만 알고 application은 service만 아는 구조로 바꿀 것이다.
두 번째는 Entity와 DTO를 제대로 분리하지 않았다. controller → service와 같이 컨트롤러는 서비스를 알고 서비스는 컨트롤러를 모르는 구조가 좋은 구조일 것이다. 양방향으로 알게되면 언젠간 순환이 생겨 문제가 생길 수 있다. 그래서 DTO → Entity와 같이 한 방향으로만 알도록 코드를 수정할 것이다. 수정하면서 동시에 Entity나 DTO내부를 보여주는 getPassword()와 같은 코드가 서비스단에 있다면 최대한 지우고 DTO자체를 넘겨주거나 Entity자체를 넘겨주며 새로운 DTO 생성자 내부에서만 호출하도록 바꾸려고한다.
//변경 전
new MemberDto(Member.getName(), member.getEmail());
//변경 후
new MemberDto(Member);
Entity내부를 보여주는 것은 설계 보여주는거니까 서비스단이 모르도록 하는 것이 좋다고 생각한다.
세 번째는 static 객체 변경이다. static메소드로 Entity를 생성했는데 이걸 없애고 다시 생성자로 객체를 생성할 것이다. 이때 파라미터로 필드를 넘겨주는 것이 아닌, 객체 자체를 넘겨줘서 내부를 최대한 보여주지 않을 것이다. 최근 GC에 대해 공부하다가 정적 객체는 프로그램 종료 전까지 사라지지 않고 이는 Old Generation에 존재할 가능성이 높다는 것을 알게되었다.. 물론 내가 만드는 프로젝트는 규모가 작아서 상관없지만 대규모 서비스를 생각하면 최대한 빼는 것이 좋다는 생각이 들었다. 또한 Old Generation이 이런 정적 객체 때문에 GC가 발생하면 이는 Major GC라서 시간이 오래 걸린다. 즉 stop the world시간이 길어지니 서비스가 오래 멈추는 것 자체에서 손해가 발생할 수 있다고 생각이 들었다. 이번에 자바8 말고 자바17쓰면서 List.of()와 같이 정적 메소드가 많이 생겼던데 이런 자주쓰는 라이브러리들은 괜찮지만 전체 코드에 개인이 만든 정적 메소드나 객체들이 많아지면 위험하다는 생각에 바꾸려고한다.
application단, service단 분리하기
AuthApplicationImpl
public HashMap<String, String> signin(MemberReqDto.LoginForm memberDto) {
String memberEmail = memberDto.getMemberEmail();
Member member = memberRepository.searchMember(memberEmail);
if(member == null) throw new MemberNotFoundException();
PasswordCheckerDto passwordCheckerDto = memberDto.toPasswordCheckerDto(member);
if(!passwordChecker.isCorrectPassword(passwordCheckerDto)) throw new InvalidPasswordException();
Long memberId = member.getMemberId();
HashMap<String,String> tokens = jwtUtil.makeResponseTokens(memberId, memberEmail,jwtSecret,accessExpired,refreshExpired);
LoginTokenDto loginTokenDto = LoginTokenDto.createAccessRefreshToken(memberId,tokens,refreshExpired);
loginTokenRepository.save(loginTokenDto);
return tokens;
}
AuthServiceImpl
@Override
public HashMap<String, String> signin(MemberReqDto.LoginForm memberDto) {
String memberEmail = memberDto.getMemberEmail();
Member member = memberRepository.searchMember(memberEmail);
if(member == null) throw new MemberNotFoundException();
PasswordCheckerDto passwordCheckerDto = new PasswordCheckerDto(memberDto,member);
if(!passwordChecker.isCorrectPassword(passwordCheckerDto)) throw new InvalidPasswordException();
Long memberId = member.getMemberId();
HashMap<String,String> tokens = jwtUtil.makeResponseTokens(memberId, memberEmail,jwtSecret,accessExpired,refreshExpired);
LoginTokenDto loginTokenDto = LoginTokenDto.createAccessRefreshToken(memberId,tokens,refreshExpired);
loginTokenRepository.save(loginTokenDto);
return tokens;
}
AuthApplicationImpl에서 AuthServiceImpl로 변경하고 AuthApplicationImpl에선 service를 호출하도록 했다.
추가로 바뀐 부분들이 보이는데 두 번째로 할 작업인 토큰 생성과 메소드에서 객체 내부의 파라미터를 넘겨받지 않고 객체 자체를 넘겨주도록 변경했다. 서비스단에서 해당 객체의 내부를 최대한 모르도록 작성하기 위해 노력했고 코드도 간결해진 것 같아 마음에 들었다. 또한 Exception도 커스텀으로 만들어서 처리했다. 저 상황만을 위한 Exception이기 때문이다. 추가로 토큰 생성도 어떤 토큰을 각각 생성하는지 아는 것 보다 응답해줄 토큰들이 있다라는 것만 알도록 변경했다.
나머지 코드들도 비슷한 방식으로 변경했다. service단은 각각의 트랜잭션 단위의 작업들을 처리하고 application단은 해당 service들을 조립하여 사용하고 이후에 나온 정보를 controller에 전달하며 역할과 책임을 더욱 세세하게 분리하였다.
Entity DTO 의존성 분리
기존 코드를 보면 아래와 같이 Entity에서 DTO를 만드는 메소드들이 있다.
public PostDetailQueryDto entityToDto(){
return new PostDetailQueryDto(
this.postId,
this.postName,
this.member.getMemberName(),
this.postViews,
this.getCreatedAt().format(DateTimeFormatter.ofPattern("yy.MM.dd HH:mm")),
this.postContent
);
}
위의 코드를 아래와 같이 DTO에서 하도록 변경하였다.
public PostDetailQueryDto(Post selectPost) {
this.postId = selectPost.getPostId();
this.postName = selectPost.getPostName();
this.memberName = selectPost.getMember().getMemberName();
this.postViews = selectPost.getPostViews();
this.createdAt = selectPost.getCreatedAt().format(DateTimeFormatter.ofPattern("yy.MM.dd HH:mm"));
this.postContent = selectPost.getPostContent();
}
application단과 service단의 분리와 같이 DTO → Entity의 방향으로만 알 수 있도록 한 것이다. 만약 서로 알게되고 서로 이것 저것 생성하다보면 순환이 생길 수 있고 이는 서비스 장애로 이어질 수도 있기 때문이다.
그리고 Entity는 데이터베이스와 직접적으로 관련 있으니 아무 것도 관련 없이 순수하게 만드는게 좋다는 생각이다.
정적 팩토리 메소드(static 메소드) 변경
public static Member createMember(String memberEmail, String memberPassword, String memberName, String memberSalt, MemberInfo memberInfo){
return new Member(memberEmail,memberPassword,memberName,memberSalt, memberInfo);
}
기존 코드는 Entity객체 생성에서 특정 메소드로만 생성할 수 있도록 했는데 지금 보면 캡슐화도 크게 안된 것 같고 static을 무분별하게 사용하면 좋지 않을 것이다라는 생각에 아래와 같이 바꾸었다.
public MemberInfo toMemberInfo(){
return new MemberInfo(this.memberAge);
}
public Member toMember(String encodedPassword, String salt, MemberInfo memberInfo){
return new Member(this.getMemberEmail(),encodedPassword,this.memberName,salt,memberInfo);
}
위 메소드들은 입력으로 들어온 DTO를 Entity로 변환해주는 메소드이다. static이 아니고 DTO객체의 내부도 최대한 보여주지 않으려 노력했다.
위의 메소드들은 회원 가입에서 사용했다.
@Override
@Transactional
public void signup(MemberReqDto.JoinForm memberDto){
Member member = memberRepository.searchMember(memberDto.getMemberEmail());
if(member != null){
throw new DuplicateMemberException();
}
String salt = randomSalt.getSalt();
String encodedPassword = seunghPasswordEncoder.encryptPassword(memberDto.getMemberPassword(),salt);
MemberInfo memberInfo = memberDto.toMemberInfo();
Member saveMember = memberDto.toMember(encodedPassword,salt,memberInfo);
memberRepository.save(saveMember);
}
마찬가지로 아래의 이전 코드와 비교해보면 코드가 조금 짧아지고 가독성이 조금 좋아진 것 같다고 생각한다.
회원 가입 기존코드
@Transactional
public void signup(MemberReqDto.JoinForm memberDto){
String memberEmail = memberDto.getMemberEmail();
String memberPassword = memberDto.getMemberPassword();
String memberName = memberDto.getMemberName();
int memberAge = memberDto.getMemberAge();
Member member = memberRepository.findMemberByMemberEmail(memberEmail);
if(member != null){
throw new DuplicateMemberException(memberEmail);
}
String salt = randomSalt.getSalt();
String encodedPassword = seunghPasswordEncoder.encryptPassword(memberPassword,salt);
MemberInfo memberInfo = MemberInfo.createMemberInfo(memberAge);
Member saveMember = Member.createMember(memberEmail,encodedPassword,memberName,salt, memberInfo);
memberRepository.save(saveMember);
}
나머지 부분들도 차례대로 바꾸고 쿼리도 수정하고 개선해야겠다.
'Java > Spring boot 프로젝트' 카테고리의 다른 글
[리팩토링 3] 쿼리 수정 및 최적화와 JMeter를 사용한 비교-게시판 만들기(16) (0) | 2023.08.09 |
---|---|
[리팩토링 1] nGrinder삽질 + JMeter 부하 테스트 해보기 - 게시판 만들기(14) (0) | 2023.08.09 |
@EntityListener로 생성일, 수정일 자동으로 넣기 + 프록시 활용(연관관계 Insert) - 게시판 만들기(13) (0) | 2023.06.29 |
Spring Security + JWT 적용하기(3) - 게시판 만들기(12) (0) | 2023.04.15 |
Spring Security + JWT 적용하기(2) - 게시판 만들기(11) (0) | 2023.04.06 |