반응형

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

사실 제 코드에는 숨겨진 비밀이 있었습니다😮.

 

그간 throw new BusinessException을 썼지만, 정작 Postman을 통해 api 테스트를 하고, Exception을 발생시켜보면 500 Error가 발생합니다.

 

제 프로그램은 client의 잘못도 server가 떠안는 구조이기 때문입니다,,^^,,

 

농담이고, JWT 로그아웃까지 구현된 지금 Error Code 반환을 처리해보면 좋을 것 같아 정리해봅니다.

 

ErrorCode.java
@Getter
public enum ErrorCode {
    // 회원
    DUPLICATED_EMAIL(HttpStatus.BAD_REQUEST, "이미 존재하는 이메일 입니다."),
    NOT_EQUAL_PASSWORD(HttpStatus.BAD_REQUEST,"입력한 비밀번호가 상이합니다."),
    CHECK_LOGIN_ID_OR_PASSWORD(HttpStatus.NOT_FOUND, "아이디 또는 비밀번호를 확인해주세요."),
    NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."),
    LOGOUT_MEMBER(HttpStatus.NOT_FOUND, "로그아웃한 회원입니다."),
    INVALID_ACCESS_TOKEN(HttpStatus.NOT_FOUND, "유효하지 않은 Access Token입니다."),
    INVALID_REFRESH_TOKEN(HttpStatus.NOT_FOUND, "유효하지 않은 Refresh Token입니다.")
    ;
    
    private final HttpStatus httpStatus;
    private final String message;

    ErrorCode(HttpStatus httpStatus, String message) {
        this.httpStatus = httpStatus;
        this.message = message;
    }
}

필요한 ErrorCode를 Enum 타입으로 선언해둡니다.

Enum은 단순히 값대신 문자열만 쓰는 줄 알았는데, 인스턴스처럼 쓸 수도 있었습니다.

 

📝 예시

ErrorCode errorCode = ErrorCode.NOT_FOUND_AUTHORITY;
HttpStatus status = errorCode.getStatus();
String message = errorCode.getMessage();

System.out.println("Status: " + status); // 출력: Status: 404 NOT_FOUND
System.out.println("Message: " + message); // 출력: 존재하지 않는 권한입니다.

 

BusinessException.java
@Getter
@RequiredArgsConstructor
public class BusinessException extends RuntimeException{
    private final ErrorCode errorCode;
}

Error Code를 사용할 RuntimeException을 상속받은 BusinessException을 선언합니다.

 

 

여기까지만 구현하면, 고객님들에게 응답을 들려드릴 수 없기에 Response도 구현해두어야 합니다.

 

ErrorResponse.java
@Getter
public class ErrorResponse {
    private String errorMessage;

    public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
        return ResponseEntity.status(errorCode.getHttpStatus())
                .body(ErrorResponse.builder()
                    .errorMessage(errorCode.getMessage())
                    .build());

    }
}

 

Reponse를 구현했으면, 전역 예외 처리가 필요합니다.

이를 위해 @RestControllerAdvice를 사용해 예외를 처리하는 핸들러를 추가해야 합니다.

 

GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> businessExceptionHandler(BusinessException e) {
        return ErrorResponse.toResponseEntity(e.getErrorCode());
    }
}

이렇게 서버는 과중한 책임에서 벗어나 400 에러도 반환할 수 있게 되었습니다.

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://github.com/dbswhd4932/shoppingmall_project

 

GitHub - dbswhd4932/shoppingmall_project: Java Springboot Jpa 를 이용한 쇼핑몰 API 프로젝트

Java Springboot Jpa 를 이용한 쇼핑몰 API 프로젝트. Contribute to dbswhd4932/shoppingmall_project development by creating an account on GitHub.

github.com

 

 

반응형
반응형

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

스마트 루피를 데려온 이유는 스마트하게 로그아웃을 완성했기 때문입니다.

 

JWT와 로그아웃(1) 게시물을 8월 23일에 작성하였는데, 약 3주의 시간이 흘렀습니다,,

 

사실 구현은 이전에 했었는데, 멋진 코드를 붙여넣다보니 내 것인듯 내 것 아닌 내 것 같은 코드가 되어버려서 리리팩토링도 하고, SonarQube 등 설치도 하고.. 스프링 공부도 하고.. 그랬습니다..

 

 

서론은 여기서 마치고, 로그아웃 코드를 정리해보도록 하겠습니다.

 

https://hj0216.tistory.com/939

 

[1년 후 마실가실] JWT와 로그아웃(1) Redis 설정

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.   마실가실의 숨겨진 비밀..한 번 로그인 한 고객님은.. 서버를 내리기 전까지 로그아웃할 수 없습니다. 이제는

hj0216.tistory.com

https://hj0216.tistory.com/945

 

[1년 후 마실가실] JWT와 로그아웃(2) RefreshToken

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.  이것저것 좋아보이는 코드를 붙여넣다보니, 이도저도 아닌 누구세요 코드가 되어 정리를 좀 했습니다,,이름하여

hj0216.tistory.com

 

UserController.java
@RestController
@RequestMapping("api/v2/users")
@RequiredArgsConstructor
public class UserController {
    @PostMapping("/logout")
    @ResponseStatus(HttpStatus.OK)
    public void logout(@RequestBody TokenInfo logoutRequestDto){
        userService.logout(logoutRequestDto);
    }
}

 

UserService.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class UserService {
    public void logout(TokenInfo logoutRequestDto) {
        if (!jwtTokenProvider.isValidAccessToken(logoutRequestDto.getAccessToken())) {
            throw new BusinessException(INVALID_ACCESS_TOKEN);
        }

        if (redisUtil.get("RT:" + logoutRequestDto.getRefreshToken()) != null) {
            redisUtil.delete("RT:" + logoutRequestDto.getRefreshToken());
        }

        Long expiration = jwtTokenProvider.getExpiration(logoutRequestDto.getAccessToken());
        redisUtil.setBlackList("AT:" + logoutRequestDto.getAccessToken(), "logout", expiration);
    }
}

1. 로그아웃 요청 전 Access Token의 유효성 검증

2. Refresh Token을 통한 Access Token 재발급을 막기 위해 Redis에 저장된 Refresh Token이 있으면 삭제

3. Redis에 AccessToken을 BlackList 방식으로 저장하여 해당 토큰을 기반으로 한 Login 방지

* BlackList 방식

특정 토큰을 무효화하기 위해 그 토큰을 블랙리스트에 추가하는 것

 

JwtTokenProvider.java
@Component // Spring이 이 클래스를 자동으로 스캔하고 빈으로 등록
@RequiredArgsConstructor
public class JwtTokenProvider {
    // 토큰 정보를 검증하는 메서드
    public boolean isValidAccessToken(String accessToken) {
        if(redisUtil.hasKeyBlackList("AT:" + accessToken)){
            return false;
        }

        try {
            parseClaims(accessToken);

            return true;
        } catch (MalformedJwtException e) {
            throw new BusinessException(MALFORMED_JWT);
        } catch (ExpiredJwtException e) {
            throw new BusinessException(EXPIRED_JWT);
        } catch (UnsupportedJwtException e) {
            throw new BusinessException(UNSUPPORTED_JWT);
        } catch (IllegalStateException e) {
            throw new BusinessException(ILLEGAL_STATE_JWT);
        }
    }

    private Claims parseClaims(String token) {
        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public Long getExpiration(String token) {
        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        // Token 남은 유효시간
        Date expiration = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getExpiration();

        // 현재 시간
        Long now = new Date().getTime();

        return (expiration.getTime() - now); // TimeUnit.MILLISECONDS
    }
}

Access Token의 유효성 검사를 통해

  * 로그아웃한 사용자인지 확인

  * token을 통해 claims(JWT 를 이용해 전송되는 암호화된 정보) 추출 시, Exception 발생 여부 확인

 

JwtAuthenticationFilter.java
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    // JWT 토큰의 인증 정보를 현재 쓰레드의 SecurityContext에 저장하는 역할 수행
    @Override
    public void doFilterInternal(HttpServletRequest request
            , HttpServletResponse response
            , FilterChain filterChain
    ) throws IOException, ServletException {
        String accessToken = SecurityUtils.resolveToken(request);

        if (accessToken != null) {
            if(jwtTokenProvider.isValidAccessToken(accessToken)){
                Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } else {
                throw new BusinessException(LOGOUT_MEMBER);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Filter에서 AccessToken의 유효성 검사를 수행하도록 하여 Logout한 멤버인지 확인

 

⭐ 좀 더 생각해봐야 할 부분 ⭐

* 필터에 유효성 검사를 추가하면 무조건 검사가 진행되는데, JwtTokenProvider에 추가적으로 수행한다면 중복 로직이 아닐까?

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://slimeland.tistory.com/41

 

[개발]JWT 갱신 토큰 관리 방법(5)-블랙리스트 사용

JWT (JSON Web Tokens) 인증 시스템에서 토큰을 무효화하는 방법 중 하나는 블랙리스트(blacklist)를 사용하는 것입니다. 이 방법은 특정 토큰을 무효화하기 위해 그 토큰을 블랙리스트에 추가하는 것을

slimeland.tistory.com

https://jake-seo-dev.tistory.com/77

 

JWT(Json Web Token) 이란 무엇이며 왜 사용하는가?

JWT (JSON Web Token) 란 무엇인가? open standard 로 RFC 7519 에 표준에 관한 내용이 기재되어 있다. 통신에 JSON 을 이용하여 JSON 객체를 통해 두 당사자 간 정보를 보안이 적용된 안전한 방식으로 전달한다.

jake-seo-dev.tistory.com

 

 

반응형
반응형

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

 

이것저것 좋아보이는 코드를 붙여넣다보니, 이도저도 아닌 누구세요 코드가 되어 정리를 좀 했습니다,,

이름하여 리리팩토링,,!

 

로그아웃하기 전 Refresh Token을 통한 Access Token 재발급을 정리하고, 다음은 정말.. 찐.. 최종.. 로그아웃..!

 

지난번에 설정한 Redis와 함께 토큰 재발행.. 도전..!

 

  • UserController.java
@RestController
@RequestMapping("api/v2/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;
	
    @PostMapping("/re-issue")
    @ResponseStatus(HttpStatus.OK)
    public TokenInfo reissue(@RequestBody TokenInfo reIssueDto) {
        return userService.reissue(reIssueDto);
    }
}

 

 

  • UserService.java
public TokenInfo reissue(TokenInfo reIssueDto) {
    try {
        jwtTokenProvider.getExpiration(reIssueDto.getAccessToken());
        throw new BusinessException(VALID_ACCESS_TOKEN);
    } catch (ExpiredJwtException e) {
        // Refresh Token 유효성 검사
        if (!jwtTokenProvider.isValidToken(reIssueDto.getRefreshToken())) {
            throw new BusinessException(INVALID_REFRESH_TOKEN);
        }

        // Redis에 Refresh Token 존재 확인
        boolean hasStoredRefreshToken = redisTemplate.hasKey("RT:" + reIssueDto.getRefreshToken());
        if(!hasStoredRefreshToken) {
            throw new BusinessException(LOGOUT_MEMBER);
        }

        String email = redisTemplate.opsForValue().get("RT:" + reIssueDto.getRefreshToken());
        User user = userRepository.findByEmail(email).orElseThrow(
                () -> new BusinessException(NOT_FOUND_MEMBER));

        // AccessToken 재발급

        // User의 role -> 스프링시큐리티의 GrantedAuthority로 변경
        // 여러개의 role을 가질수 있으므로 Set
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                user.getEmail(),
                user.getPassword(),
                Set.of(SecurityUtils.convertToAuthority(user.getRole()))
        );

        return jwtTokenProvider.generateAccessToken(userDetails);
    }
}

재생성.. 이제 자신있습니다..!

 

Refresh Token의 목적이 Access Token 만료 시, 토큰을 재발행 해주는 것이기 때문에, Access Token 만료 전에는 재발행할 수 없도록 하였습니다.

⭐ Access Token이 만료될 경우, Refresh Token의 유효성 검사를 수행합니다.

⭐ Redis 서버를 조회하여, 로그아웃한 User인지 확인합니다.

 

⭐ 토큰을 재발급받을 때,

Access Token만 재발급할 수도, 기존의 Refresh Token을 삭제하고 Access Token과 Refresh Token을 재발급할`수도 있습니다.

저는 로그인된 상태가 계속 유지되는 것이 한 번 Access Token이 탈취되면 보안 상 위험할 수 있다고 판단하여, Refresh Token까지 만료된 경우에는 다시 로그인을 하여 Access Token과 Refresh Token을 발급받도록 하였습니다.

 

  • JwtTokenProvider.java
@Component // Spring이 이 클래스를 자동으로 스캔하고 빈으로 등록
@RequiredArgsConstructor
public class JwtTokenProvider {
    private static final String AUTHORITIES_KEY  = "roles";
    private static final String BEARER_TYPE = "Bearer";

    @Value("${jwt.secret}")
    private String jwtSecretKey;

    private final RedisUtil redisUtil;
    private final UserRepository userRepository;

    // 유저 정보를 가지고 AccessToken, RefreshToken을 생성하는 메서드
    public TokenInfo generateToken(UserDetails auth) {
        // Access Token 생성
        String accessToken = generateAccessToken(auth).getAccessToken();

        // Refresh Token 생성
        // 아무런 정보도 토큰에 넣지 않고, 단순히 IssuedAt과 Expiration만을 입력한 후 서명
        String refreshToken = generateRefreshToken();

        return TokenInfo.builder()
                .grantType(BEARER_TYPE)
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    public String generateRefreshToken() {
        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        Date now = new Date();
        Date expirationRefreshToken = new Date(now.getTime() + Duration.ofMinutes(2).toMillis());

        return Jwts.builder()
                .setIssuedAt(now)
                .setExpiration(expirationRefreshToken)
                .signWith(secretKey)
                .compact();
    }

    public TokenInfo generateAccessToken(UserDetails auth) {
        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        Date now = new Date();
        Date expirationAccessToken = new Date(now.getTime() + Duration.ofMinutes(1).toMillis());

        String authorites = auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        String accessToken = Jwts.builder()
                .setSubject(auth.getUsername()) // 토큰 소유자 설정
                .claim(AUTHORITIES_KEY, authorites) // JWT 내부에 추가적인 정보 설정
                .setIssuedAt(now)
                .setExpiration(expirationAccessToken)
                .signWith(secretKey)
                .compact();

        return TokenInfo.builder()
                .grantType(BEARER_TYPE)
                .accessToken(accessToken)
                .build();
    }

    // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
    public Authentication getAuthentication(String accessToken) {
        //유저 정보 부분만 가져옴
        Claims claims = parseClaims(accessToken);
        if(claims == null)
            throw new BusinessException(NOT_FOUND_MEMBER);

        if (claims.get(AUTHORITIES_KEY) == null)
            throw new BusinessException(NOT_FOUND_AUTHORITY);

        // 클레임에서 권한 정보 가져오기
        Set<GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY)
                        .toString()
                        .split(","))
                        .map(SecurityUtils::convertToAuthority) // 스트림의 각 요소를 다른 형태로 변환
                        .collect(Collectors.toSet());


        // UserDetails 객체를 만들어서 Authentication 리턴
        UserDetails userDetails = UserPrinciple.builder()
                .email(claims.getSubject())
                .authorities(authorities)
                .build();

        return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
        // credentials: null, 인증이 완료된 후에는 실제 자격 증명(예: 비밀번호)을 사용할 필요가 없으므로 null로 설정
    }
    /**
     * UsernamePasswordAuthenticationToken
     * userDetails: 사용자의 정보(예: 이메일, 권한)를 담고 있는 객체
     * null: 이 위치에 보통은 비밀번호나 인증 토큰이 들어가지만, 여기서는 이미 인증이 된 상태이므로 필요하지 않아 null로 설정
     * authorities: 사용자가 가지고 있는 권한 목록
     * */

    // 토큰 정보를 검증하는 메서드
    public boolean isValidToken(String token) {
        if(token == null || token.isEmpty())
            return false;

        try {
            return (!parseClaims(token).isEmpty());
        } catch (MalformedJwtException e) {
            throw new BusinessException(MALFORMED_JWT);
        } catch (ExpiredJwtException e) {
            throw new BusinessException(EXPIRED_JWT);
        } catch (UnsupportedJwtException e) {
            throw new BusinessException(UNSUPPORTED_JWT);
        } catch (IllegalStateException e) {
            throw new BusinessException(ILLEGAL_STATE_JWT);
        }
    }

    // 리퀘스트의 토큰에서 암호풀어 Claims 가져오기
    private Claims parseClaims(String token) {
        if(redisUtil.hasKeyBlackList(token)){
            throw new BusinessException(LOGOUT_MEMBER);
        }

        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public Long getExpiration(String token) {
        Key secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));

        // Token 남은 유효시간
        Date expiration = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getExpiration();

        // 현재 시간
        Long now = new Date().getTime();

        return (expiration.getTime() - now); // TimeUnit.MILLISECONDS
    }
}



 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://jjhwang.tistory.com/15

 

[JWT 재발급 01] access token 재발급 구현

먼저 access token과 refresh token이 저장되는 위치를 확인해 보자 Access Token : Header에 저장 Refresh Token : 쿠키와 DB에 저장 | access token 재발급 순서 1. header에 담긴 access token 유효기간 확인 - 유효기간이

jjhwang.tistory.com

https://inkyu-yoon.github.io/docs/Language/SpringBoot/RefreshToken#redis-%EC%82%AC%EC%9A%A9-vs-memcached

 

· Redis를 활용해서 Refresh Token 구현하기

👩🏻‍💻 지식 창고 📚

inkyu-yoon.github.io

 

반응형
반응형

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

리팩토링을 함께할 친구를 사귀었습니다.

그간 혼자 진행하면서, 잘하고 있는 게 맞을까 고민하는 시간이 많았는데 이제는 SonarQube가 그 고민을 어느정도 해결해줄 거라 기대하고 있습니다.😆.

 

이 친구의 정의는 ' 버그, 코드 스멜, 보안 취약점을 발견할 목적으로 정적 코드 분석으로 자동 리뷰를 수행하기 위한 지속적인 코드 품질 검사용 오픈 소스 플랫폼'입니다.

 

간단하게 설정 방법과 IntelliJ에서 플러그인으로 설치하는 방법, 그리고 어떤 식으로 사용했는지까지 간단히 정리해 보고자 합니다🫠.

 

1. SonarQube Comminity Edition 다운로드

https://www.sonarsource.com/products/sonarqube/downloads/

 

Download | SonarQube

Get the latest LTS and version of SonarQube the leading product for Code Quality and Security from the official download page.

www.sonarsource.com

 

2. SonarQube CLI Window x64 다운로드(저는 윈도우를 씁니다🫠)

https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner/

 

SonarScanner CLI

The SonarScanner CLI is the scanner to use when there is no specific scanner for your build system.

docs.sonarsource.com

 

3. SonarQube 서버 띄우기

sonarqube-10.6.0.92116\sonarqube-10.6.0.92116\bin\windows-x86-64\StartSonar.bat 실행

'SonarQube is operational' 문구가 뜰 때까지 대기

 

4. localhost:9000 접속

초기 아이디: admin

초기 비밀번호: admin

 

5. Create Project

 

저는 이미 마실가실 리팩토링 프로젝트를 생성한 상태입니다!

 

프로젝트 소스코드 위치 선택: 저는 로컬 파일을 대상으로 할 것이기 때문에 Mannually를 선택하였습니다.

CI와 통합할 수도 있지만, 저는 로컬 파일을 대사응로 SonarQube를 사용할 것이기 때문에 Locally를 선택하였습니다.

그 다음으로는 토큰을 생성하고(🚨토큰 값은 어디에 적어둡니다),

빌드 옵션에 따라서, 추가적인 작업을 진행합니다.

 

저는 gradle을 쓰고 있기 때문에,

* build.gradle

plugins {
	id "org.sonarqube" version "5.0.0.4638"
}

 

플러그인을 추가하고, 빌드를 해줍니다.

🚨빌드를 하지 않으면 SonarQube관련 명령어 입력 후 빌드 실패가 발생하니 빌드를 해줘야 합니다.

 

build.gradle 파일이 위치한 곳에서 

PowerShell을 켜서 gradle 관련 명령어를 입력합니다.

  * 플러그인 아래에 명령어가 적혀있습니다.

 

+ IntelliJ에서 Plugin으로 SonarQube를 설치할 수 있습니다.

 

이젠 더이상 혼자가 아닙니다.

저에겐 리팩토링 친구가 생겼습니다🤓.

 

++ SonarQube 문제 해결 순서

1. 가장 최근 분석 결과부터 수정

2. Duplication부터 수정

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

https://samori.tistory.com/78

 

SonarQube 사용법 (Windows)

서론 Static Code Analysis Tool, 정적코드분석툴로 불리는 이 툴은 보통 개발자가 개발을 하고 배포하기 전에 프로젝트 소스 코드에 문제가 있는지, 있다면 어떠한 문제가 있는지 분석하고 체크하기

samori.tistory.com

https://velog.io/@ttanggin/HowtoStartSonarQube

 

SonarQube 시작하기

Windows 환경에서 Java 11 Maven 프로젝트에 SonarQube를 사용하여 정적분석을 해보자!

velog.io

https://brunch.co.kr/@joypinkgom/207

 

소스 정적 분석도구 SonarQube

v9.9 리뉴얼 | SonarQube 관련글은 이미 두 번이나 작성했지만 다시 한번 글을 판다. 최근 새로운 플랫폼을 기획하면서 SonarQube를 다시 설치할 일이 생겼는데, 메이저 버전이 10이 됐더라. 처음 SonarQub

brunch.co.kr

 

반응형
반응형

1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.

 

 

JWT를 사용해서 로그인과 로그아웃 구현에 성공하였으나..

 

Redis 추가 설정, Spring Security 추가 설정, 리팩토링 전과 후의 Controller 등..

쓰지 않거나 리팩토링 대상이 아닌 파일들을 삭제할 겸 패키지 구조도 재정비를 하였습니다.

 

패키지 구성 관련해서는 공부해본 적이 없어서 이참에 간단하게 정리해보고자 합니다!

 

 

패키지 구조 종류

1. 계층형

각 계층을 대표하는 디렉터리를 기준으로 패키지 구성

  * 예: controller 패키지 안에 UserController, ScheduleController 등 모든 Controller가 위치

  * 장점: 프로젝트에 대한 이해가 상대적으로 낮아도 전체적인 구조를 빠르게 파악할 수 있음

  * 단점: 패키지에 너무 많은 클래스들이 모일 수 있음

             도메인과 관련된 변경이 발생했을 경우, 여러 패키지에서 수정이 발생

 

2. 도메인형

도메인을 기준으로 패키지 구성

  * 예: user 패키지 안에 controller, service, repository 패키지가 존재

  * 장점: 관련된 코드들이 응집해 있음

            도메인과 관련된 변경이 발생했을 경우, 하나의 패키지에서 수정이 발생(= 변경 범위가 적음)

  * 단점: 프로젝트에 대한 이해도가 낮을 경우 전체적인 구조를 파악하기 어려움

            개발자의 관점에 따라 어느 패키지에 둘지 애매한 클래스들이 존재

 

기존 패키지 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
├─admin
│  ├─controller
│  ├─dao
│  └─service
├─api
│  └─controller
├─main
│  ├─controller
│  ├─dao
│  └─service
├─msgs
│  ├─chatbot
│  ├─dto
│  ├─entity
│  │  ├─destination
│  │  ├─review
│  │  ├─schedule
│  │  └─user
│  ├─error
│  ├─imageupload
│  ├─jwt
│  │  ├─controller
│  │  └─service
│  └─redis
├─mypage
│  ├─controller
│  ├─dao
│  ├─dto
│  └─service
├─transport
│  ├─controller
│  ├─dao
│  └─service
├─tripplace
│  ├─controller
│  ├─dao
│  └─service
├─tripschedule
│  ├─controller
│  ├─dao
│  ├─repository
│  └─service
├─tripstory
│  ├─controller
│  ├─dto
│  ├─repository
│  └─service
└─user
    ├─controller
    ├─dao
    ├─repository
    └─service

 

 

변경된 패키지 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
├─domain
│  ├─tripschedule
│  │  ├─controller
│  │  ├─domain
│  │  ├─dto
│  │  ├─exception
│  │  ├─repository
│  │  └─service
│  ├─tripstory
│  │  ├─controller
│  │  ├─domain
│  │  ├─dto
│  │  ├─exception
│  │  ├─repository
│  │  └─service
│  └─user
│      ├─controller
│      ├─domain
│      ├─dto
│      ├─exception
│      ├─repository
│      └─service
├─global
│  ├─common
│  │  ├─error
│  │  ├─jwt
│  │  ├─model
│  │  └─redis
│  ├─config
│  └─util
└─infra
    ├─chatbot
    └─imageupload

 

 

 

🙋‍♀️

본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.

 

📑

참고 자료

 

[아키텍쳐] 패키지 구조 : 계층형 VS 도메인형 어떤 것을 선택할까?

🎯 0. 들어가기 전 MVC 패턴 & 자바 기반의 콘솔 애플리케이션에서는 관성적으로 model(domain) & controller & view 패키지를 만들고 시작하는 경우가 대부분이었다. 웹 애플리케이션을 구현하면서, 설계

ksh-coding.tistory.com

 

Spring Guide - Directory - Yun Blog | 기술 블로그

Spring Guide - Directory - Yun Blog | 기술 블로그

cheese10yun.github.io

 

Java Practices->Package by feature, not layer

Package by feature, not layer The first question in building an application is "How do I divide it up into packages?". For typical business applications, there seems to be two ways of answering this question. Package By Feature Package-by-feature uses pack

www.javapractices.com

 

반응형