Skip to content

refactor: User 리펙터링#136

Merged
mete0rfish merged 17 commits intoICT-Dev-Route:mainfrom
mete0rfish:main
Jan 4, 2025
Merged

refactor: User 리펙터링#136
mete0rfish merged 17 commits intoICT-Dev-Route:mainfrom
mete0rfish:main

Conversation

@mete0rfish
Copy link
Member

@mete0rfish mete0rfish commented Jan 4, 2025

#️⃣ Issue

🔎 작업 내용

  • AuthToken 삭제
  • CustomUserDetails을 record로 변경
  • User의 Valid 어노테이션 변경
  • DTO를 record로 변경 및 @builder 클래스에 설정
  • 불필요한 import 삭제
  • DevelopField, LoginType의 Enum 클래스 내에서 필드를 매핑하도록 로직 추가
  • UserController의 외부 로그인 API 통합
  • CustomUserDetailsService, UserService 메서드 분리
  • UserLoginRequest을 불변 객체로 변경

🤔상세 내용

엔티티

@NotNullrhk 과 @column(nullable = flase)

@NotNull @column(nullable = flase)
목적 애플리케이션 레벨의 유효서 검증 데이터베이스 레벨의 무결성 제약 조건
동작 주체 Hibernate Validator 등 검증 도구 JPA와 데이터베이스
검증 시점 런타임 (Java Bean 검증 시점) 데이터베이스에 저장하거나 테이블 생성 시점
스키마 생성 여부 데이터베이스에는 영향 없음 데이터베이스에 NOT NULL 적용

기존 User에서 @NotNull을 통해서만 null 검증을 진행했다. 이 경우, 데이터베이스에는 제약 조건이 설정되지 않기 때문에 두 가지를 모두 사용하여 프론트에서 정보를 받아와서 Serializable 시, 데이터베이스에 저장 시 모두에 검증할 수 있도록 수정했다.

  • Password의 경우, SSO 인증 시에는 비밀번호가 저장되지 않기 때문에 nullable하도록 설정
  • @column에 별도의 옵션이 추가되지 않는 경우, @column을 삭제하여 가독성 향상

@DaTa 도입 여부

💡@DaTa

💡Record

  • Private final, public Getter, toString, Equals(), HashCode(), 모든 필드 생성자
  • Immutable 객체 → 불변 객체

따라서, @DaTa와 Record 사용 시 불변 객체의 사용 여부를 통해 적절하게 둘 중 하나를 선택하면 될 것 같다.

@builder

Record의 경우, DTO로 사용되고 세부적인 필드 조절이 필요하지 않기 때문에 생성자 대신 record 위에 @builder를 추가하도록 수정

public record UserMyPageResponse(
        List<BookmarkCompany> companies,
        List<BookmarkVideo> videos,
        List<BookmarkRoadmap> roadmaps
) {

    @Builder
    public UserMyPageResponse(List<BookmarkCompany> companies, List<BookmarkVideo> videos, List<BookmarkRoadmap> roadmaps) {
        this.companies = companies;
        this.videos = videos;
        this.roadmaps = roadmaps;
    }

    public static UserMyPageResponse of(User user){
        Bookmark bookmark = user.getBookmark();
        return UserMyPageResponse.builder()
                .companies(bookmark.getCompanies())
                .videos(bookmark.getVideos())
                .roadmaps(bookmark.getRoadmaps())
                .build();
    }
}

@NoArgsConstructor(access = AccessLevel.PROTECTED)

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AuthToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String accessToken;
    private String refreshToken;
    private LocalDateTime expired_date;

    @Builder
    public AuthToken(String accessToken, String refreshToken, LocalDateTime expired_date) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        this.expired_date = expired_date;
    }
}
  • 뜻 : 아무런 매개변수가 없는 생성자를 생성하되 다른 패키지에 소속된 클래스는 접근을 불허한다”

    protected AuthToken() {}
  • 사용 이유

    • Entity의 Proxy 조회 때문에 사용
    • 지연 로딩 시, 프록시 객체를 생성하여 접근하게 된다. 만약 Private일 경우, 프록시 객체 생성이 불가능하기 때문에 에러가 발생
    • 현재는 상관 없지만

Enum

매핑을 위한 Map 도입

각 직군에 해당하는 문자열을 Enum 값으로 변경하는 코드가 아래와 같다.

public enum DevelopField {
    BACKEND,
    FRONTEND,
    MOBILE,
    AI,
    DATA_SCIENCE,
    NONE;

    public static DevelopField toEnum(String type) {
        return switch (type) {
            case "backend" -> DevelopField.BACKEND;
            case "frontend" -> DevelopField.FRONTEND;
            case "ai" -> DevelopField.AI;
            case "mobile" -> DevelopField.MOBILE;
            case "datascience" -> DevelopField.DATA_SCIENCE;
            default -> DevelopField.NONE;
        };
    }

    public static DevelopField toEnumBySearchKeyWord(String type) {
        return switch (type) {
            case "백엔드" -> DevelopField.BACKEND;
            case "프론트엔드" -> DevelopField.FRONTEND;
            case "인공지능" -> DevelopField.AI;
            case "모바일" -> DevelopField.MOBILE;
            case "데이터" -> DevelopField.DATA_SCIENCE;
            default -> DevelopField.NONE;
        };
    }
}

switch 사용 시, 컴파일 최적화 여부에 따라 O(logN)이 소요될 수 있다. 또한, 확장 시 추가로 작성해야 하는 부분이 많아지 기 때문에 상대적으로 Fix된 코드이다. 단순한 데이터 매핑 시, Map을 통해 항상 O(1)로 고정된 성능을 가지도록 리펙터링한다.

EnumMap 사용에 관해

EnumMap은 Enum 값을 Key로 사용한다. 예를 들어 {DevelopField.BACKEND, “backend”} 처럼 사용된다. 그러나 해당 코드는 Enum 값을 Value로 사용하기 때문에 EnumMap 사용 시 부자연스럽다.

리펙터링 후

public enum DevelopField {
    BACKEND,
    FRONTEND,
    MOBILE,
    AI,
    DATA_SCIENCE,
    NONE;

    private static final Map<String, DevelopField> TYPE_MAP = Map.of(
            "backend", BACKEND,
            "frontend", FRONTEND,
            "ai", AI,
            "mobile", MOBILE,
            "datascience", DATA_SCIENCE
    );

    private static final Map<String, DevelopField> SEARCH_KEYWORD_MAP = Map.of(
            "백엔드", BACKEND,
            "프론트엔드", FRONTEND,
            "인공지능", AI,
            "모바일", MOBILE,
            "데이터", DATA_SCIENCE
    );

    public static DevelopField toEnum(String type) {
        return TYPE_MAP.getOrDefault(type.toLowerCase(), NONE);
    }

    public static DevelopField toEnumBySearchKeyWord(String searchKeyword) {
        return SEARCH_KEYWORD_MAP.getOrDefault(searchKeyword.toLowerCase(), NONE);
    }
}

Controller

MemberController

OAuth2 API 통일

현재 Kakao, Naver, Google에 대한 메서드가 각각 존재하기 때문에 중복 로직이 발생한다.

@GetMapping("/auth/google")
    public ResponseEntity getGoogleRedirectUrl() {
        String url = userService.getRedirectUrl(LoginType.GOOGLE);
        return new ResponseEntity<>(url, HttpStatus.OK);
    }

  @GetMapping("/auth/naver")
  public ResponseEntity getNaverRedirectUrl() {
      String url = userService.getRedirectUrl(LoginType.NAVER);
      return new ResponseEntity<>(url, HttpStatus.OK);
  }

따라서, PathVariable을 통해 공통 로직으로 처리하도록 수정했다.

@mete0rfish mete0rfish added the 📄문서 문서에 수정할 부분이 있어요 label Jan 4, 2025
@mete0rfish mete0rfish merged commit 3fb8219 into ICT-Dev-Route:main Jan 4, 2025
1 check failed
@Munhangyeol
Copy link
Contributor

오 enum으로 변환할 때 걸리는 시간은 map을 이용해서 O(1)시간내로 단축한 점 인상적이네요😄😄

private static final Map<String, DevelopField> TYPE_MAP = Map.of(
            "backend", BACKEND,
            "frontend", FRONTEND,
            "ai", AI,
            "mobile", MOBILE,
            "datascience", DATA_SCIENCE
    );

    private static final Map<String, DevelopField> SEARCH_KEYWORD_MAP = Map.of(
            "백엔드", BACKEND,
            "프론트엔드", FRONTEND,
            "인공지능", AI,
            "모바일", MOBILE,
            "데이터", DATA_SCIENCE
    );

    public static DevelopField toEnum(String type) {
        return TYPE_MAP.getOrDefault(type.toLowerCase(), NONE);
    }

    public static DevelopField toEnumBySearchKeyWord(String searchKeyword) {
        return SEARCH_KEYWORD_MAP.getOrDefault(searchKeyword.toLowerCase(), NONE);
    }

그런데 혹시 TYPE_MAPSEARCH_KEYWORD_MAP이 같은 기능을 하는 것 같은데 하나의 map으로 TYPE_MAP으로 통합하는 것은 어떨까요?

private static final Map<String, DevelopField> TYPE_MAP = Map.of(
            "backend", BACKEND,
            "frontend", FRONTEND,
            "ai", AI,
            "mobile", MOBILE,
            "datascience", DATA_SCIENCE,
            "백엔드", BACKEND,
            "프론트엔드", FRONTEND,
            "인공지능", AI,
            "모바일", MOBILE,
            "데이터", DATA_SCIENCE
    );

하나의 map으로 통합하면 toEnumtoEnumBySearchKeyWordtoEnum으로 통합해서 enum클래스내에 메서드를 두개로 줄일 수 있을거 같아요

@Munhangyeol
Copy link
Contributor

@DataRecord의 차이에 대해서 명확히 잘 몰랐는데, 감사합니다:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📄문서 문서에 수정할 부분이 있어요

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants