늦은 프로그래밍 이야기

221226 TIL (FetchType) 본문

내일배움캠프/TIL, WIL

221226 TIL (FetchType)

한정규 2022. 12. 26. 21:40

개인과제 리팩토링

JWT 인증/인가 관련

인증/인가 중복되는 validateAndGetUserInfo 기능을 jwtUtil에 메소드로 구현하기

AuthenticatedUserInfoDto authenticatedUserInfoDto = jwtUtil.validateAndGetUserInfo(token);
return blogService.createPosting(requestDto, authenticatedUserInfoDto.getUsername());

JWTUTIL CLASS
public AuthenticatedUserInfoDto validateAndGetUserInfo(String token) {
  if (this.validateToken(token)) {
		Claims claims = this.getUserInfoFromToken(token);
    String username = claims.getSubject();
    UserRoleEnum role = UserRoleEnum.valueOf(claims.get("auth").toString());
    return new AuthenticatedUserInfoDto(role, username);
  } else {
    throw new IllegalArgumentException("Token Error");
  }
}

Posting 생성, 수정, 삭제 시에 인증/인가를 하는 코드가 중복이 되어 해당 기능을 JwtUtil에 메소드로 구현을 하고 Controller에서 token 값을 받아 해당 토큰에서 username과 role 만 추출하여 반환하는 메소드를 구현 하였는데 NullPointerException이 발생하였다. Cannot invoke Object.toString()이라고 나오는 것 보니 toString() 부분에 무언가 문제가 발생한 것 같아서 그 부분을 고쳐보고, 토큰을 생성하는 부분에도 role을 String타입으로 변경도 해보았지만 똑같이 에러가 발생하였다.

 

다른 수강생의 도움을 받아 화면공유를 통해 문제점을 찾아 나가다 보니 “auth”를 “Auth”로 잘못 입력하여 해당 값을 가져오지 못해서 발생하는 문제였다.. 아주 사소한 실수지만, 쉽게 발견할 수 있는 컴파일 에러가 아닌 NPE가 발생하여 정말 세심하게 보지 않으면 해결이 오래 걸리기 때문에 오타나 대소문자 구분을 조금 더 신경써서 철저히 해야겠다는 생각이 들었다. (아주 오래 걸렸다..)

 

UserRole의 ADMIN 여부 Controller에서 판별

기존의 ADMIN 여부를 판별하는 기능을 isWriter 메소드에 같이 넣어 뒀는데, isWriter는 User클래스에서 Posting과 Comment 클래스에 각각 옮겨주며 책임을 명확히 하였는데, UserRole의 ADMIN 여부 판별 기능은 Posting과 Comment의 책임과는 별개인 것이고, ADMIN의 판별을 실무에서는 Controller단에서 해결하고, ADMIN과 USER의 update와 delete 기능을 각각의 메소드로 구현하는 것이 명확하다는 피드백을 듣고 해당 기능을 분리하고, ADMIN 여부의 판별을 Controller에 구현하였다.

private boolean isAdmin(UserRoleEnum userRoleEnum) {
    return userRoleEnum == UserRoleEnum.ADMIN;
}

@PutMapping("/posts/{id}")
public PostingResponseDto updatePosting(@PathVariable Long id, @RequestBody UpdateRequestDto requestDto, HttpServletRequest request) {
    String token = jwtUtil.resolveToken(request);

    if (token == null) {
        throw new IllegalArgumentException("토큰이 유효하지 않습니다.");
    }
    AuthenticatedUserInfoDto authenticatedUserInfoDto = jwtUtil.validateAndGetUserInfo(token);
        if (this.isAdmin(authenticatedUserInfoDto.getUserRoleEnum())) {
            return blogService.updateAdmin(id, requestDto);
        } else {
            return blogService.update(id, requestDto, authenticatedUserInfoDto.getUsername());
        }
    }

처음에는 isAdmin 메소드만 구현하고, updatePosting에서 if문을 사용하여 Service의 update 메소드와 updateAdmin 메소드로 보내는 코드로 작성하였는데, 단일 책임 원칙에 맞지 않는 코드라는 얘기를 듣고 Controller부터 둘을 나눠서 API를 작성하고 진행하였다.

private boolean isAdmin(UserRoleEnum userRoleEnum) {
    return userRoleEnum == UserRoleEnum.ADMIN;
}

// ADMIN의 updatePosting
@PutMapping("/admin/posts/{id}")
public PostingResponseDto updatePostingAdmin(@PathVariable Long id, @RequestBody UpdateRequestDto requestDto, HttpServletRequest request) {
    String token = jwtUtil.resolveToken(request);

    if (token == null) {
        throw new IllegalArgumentException("토큰이 유효하지 않습니다.");
    }
    AuthenticatedUserInfoDto authenticatedUserInfoDto = jwtUtil.validateAndGetUserInfo(token);
    if (!this.isAdmin(authenticatedUserInfoDto.getUserRoleEnum())) {
        throw new IllegalArgumentException("권한이 없습니다.");
    }
    return blogService.updateAdmin(id, requestDto);
}

// USER의 updatePosting
@PutMapping("/posts/{id}")
public PostingResponseDto updatePosting(@PathVariable Long id, @RequestBody UpdateRequestDto requestDto, HttpServletRequest request) {
    String token = jwtUtil.resolveToken(request);

    if (token == null) {
        throw new IllegalArgumentException("토큰이 유효하지 않습니다.");
    }
    AuthenticatedUserInfoDto authenticatedUserInfoDto = jwtUtil.validateAndGetUserInfo(token);
    return blogService.update(id, requestDto, authenticatedUserInfoDto.getUsername());
}

객체지향에서 배웠던 SOLID에 대해 전혀 고려를 하지 않고 있었는데 이번 기회에 객체지향에 대해 다시 한번 리마인드 되는 기회가 되었던 것 같고, SOLID 원칙을 조금 더 공부해 봐야겠다는 생각이 들었다.


테이블 관계

POSTING CLASS

// 1
@ManyToOne
@JoinColumn(name = "users_username", nullable = false)
private User user;

// 2
@Column(name = "users_username")
private String username;

위의 코드에서 1과 2는 데이터베이스 상에서는 어떠한 코드를 사용하더라도 User와 Posting의 테이블이 정확히 일치하므로 테이블 상의 데이터는 어떠한 코드로 작성하더라도 User의 username을 Posting이 외래키로 가져온 것을 알 수 있다. 하지만 JPA는 1로 작성하면 User와 Posting이 연결되어 있다는 것을 알 수 있지만, 2의 경우에는 둘이 연결되어 있다는 것을 알지 못한다. 따라서 자바코드에서는 1은 연결이 되어 있는 코드고 2는 연결이 되어 있지 않은 코드이다.

 

1의 경우에는 User의 기능을 사용해야할 때 작성한다. User의 기능을 사용할 필요가 없다면 2의 코드로 작성하는 것이 합당하다. 하지만 나는 1의 코드로 작성을 했었고, User의 기능을 사용해 Posting과 Comment의 username의 일치여부를 확인하는 메소드를 작성하였지만, 잘못된 방식으로 이를 사용했다.

 

User클래스에 있는 isPostingWriter() → Posting클래스로 이전

  • AS-IS
if (user.isPostingWriter(posting)) {
    throw new IllegalArgumentException("본인이 작성한 게시글만 수정할 수 있습니다.");
}

USER CLASS
public boolean isPostingWriter(Posting posting) {
    return !this.username.equals(posting.getUser().getUsername()) && this.role == UserRoleEnum.USER;
}
  • TO-BE
if (posting.isPostingWriter(username)) {
    throw new IllegalArgumentException("본인이 작성한 게시글만 수정할 수 있습니다.");
}

POSTING CLASS
public boolean isPostingWriter(String username) {
    return !username.equals(this.getUser().getUsername());
}

Posting 클래스가 User 클래스와 연관관계를 맺었다는 건 User의 기능을 사용하겠다는 의미여서, 그걸 “누가” 사용하는가가 중요하고, Posting이 사용하기 때문에 Posting이 User를 사용해야 하므로 해당 메소드가 User에 있으면서 Posting을 사용하는 것은 옳지 않고, Posting에 존재하면서 getUser()로써 User를 사용하는 것이 바람직하기 때문에 이전하였다. 얼핏 보면 둘이 기능상 상이한 부분도 없어서 같은 것 같지만, 둘 중 조금 더 명확한 코드는 Posting에 해당 메소드가 존재하며 User를 Posting이 사용하는 것이다. (연관관계를 설정했기 때문이다.) 아직은 정확히 이해가 되지 않지만, 추후 같은 문제를 맞이하고 조금 더 찾아보고 문제를 해결해 나간다면 충분히 이해할 수 있을 것이라 기대한다.

 

하지만 만약 연관관계를 설정하지 않고 Posting에서 User를 사용하지 않는다면 코드는 아래와 같이 바뀌어야 할 것이다.

if (posting.isPostingWriter(username)) {
    throw new IllegalArgumentException("본인이 작성한 게시글만 수정할 수 있습니다.");
}

POSTING CLASS
public boolean isPostingWriter(String username) {
    return !this.username.equals(username)
}

여기서 고민해야 할 것은 “정말 Posting은 User의 정보가 필요한가” 이다. Posting이 사용하는 것은 User의 username을 writer로써로만 필요로 하는데 User의 password 정보까지 Posting이 가지고 있을 필요가 있을까를 고민해 보아야 한다. 만약 Posting의 생성, 조회, 수정, 삭제하는 기능 중 password가 필요한 메소드를 구현할 여지가 있다거나 등등의 이유가 있다면 User의 값들을 가지고 사용하면 되겠지만 그게 아니라면 그저 User의 username만 writer로써 가지고 있으면 되는 것이다.

 

연관관계에 대해 공부하기 위해서 연관관계를 할 수 있는대로 맺었지만, 사용할 필요가 없으면 연관관계를 맺는 것에 조금 더 신중해야 하고, JPA는 그저 쿼리를 쉽게 날려주는 것은 간편하지만, 이러한 연관관계의 필요성에 대해 고려하고 신중하게 연관관계를 맺어야 한다는 사실을 알게 되어서 조금 더 JPA에 대해 심층적으로 공부해야 할 것 같다는 생각이 들었다.

 

@ManyToOne 어노테이션에 (fetch = FetchType.LAZY) 설정

개인과제를 진행하면서는 LAZY와 EAGER의 차이점을 잘 모르고 일단 동작만 하면 괜찮다는 마인드였기 때문에 거의 신경을 쓰지 않아서 ManyToOne 어노테이션에 LAZY 설정을 하지 않았는데 리뷰 강의를 듣다 보니 ManyToOne은 EAGER가 디폴트 값이고 EAGER일 경우에는 Posting을 불러왔을 때 User 정보를 같이 불러오기 때문에 불필요한 쿼리가 발생한다.

  • EAGER

하지만 LAZY로 설정해두면 개발자가 User를 호출하지 않는 이상 User를 호출하는 쿼리는 발생하지 않는다.

  • LAZY

캡쳐 사진에서는 User가 2개 밖에 없기 때문에 2개의 쿼리만 발생을 하였는데 이것이 만약 1000개의 글에 1000개의 User가 존재한다면 1000개의 불필요한 쿼리가 발생하는 것이므로 ManyToOne에는 반드시 LAZY 설정을 해줘야 할 것이다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
private User user;

'내일배움캠프 > TIL, WIL' 카테고리의 다른 글

221228 TIL  (0) 2022.12.28
221227 TIL  (0) 2022.12.27
8주차 WIL  (0) 2022.12.25
221223 TIL (개인과제 리팩토링)  (0) 2022.12.23
221222 TIL  (0) 2022.12.23
Comments