Skip to content

hyuk.kim (김상혁) 과제 제출#17

Open
duckchanahn wants to merge 37 commits intoej31:mainfrom
duckchanahn:feature/hyuk
Open

hyuk.kim (김상혁) 과제 제출#17
duckchanahn wants to merge 37 commits intoej31:mainfrom
duckchanahn:feature/hyuk

Conversation

@duckchanahn
Copy link

진행상황

완료
필수: API 디자인
필수: 유효성 검사
필수: API 응답 값
옵션: Custom Exception
옵션: API Throttling
옵션: 응답 형식

미완
옵션: Test Code
옵션: 배포 후 녹화 및 댓글에 적용

- 프로젝트 기본 구조 확립
- 도메인 레이어 (domain) 추가
- 프레젠테이션 레이어 (presentation) 구성
- 공통 응답 및 예외 처리 모듈 (core) 생성
- 웹 설정을 위한 common.config 패키지 추가

아키텍처 구조:
- domain: 도메인 엔티티 클래스
- presentation: API 컨트롤러 및 DTO
- repository: 데이터 접근 계층
- service: 비즈니스 로직 계층
- core: 공통 응답, 예외 처리
ApiResponse -> SuccessResponse
- 성공 응답 : SuccessResponse
- 실패 응답 : ErrorResponse
- 요구사항에 적힌 에러를 미리 구현, 이후 수정해서 사용 예정
- BusinessException
- CustomErrorCode
- GlobalExceptionHandler
재사용성을 높이기 위해 기존 CustomErrorCode에서 Code, Message 분리
GlobalExceptionHandler의 복잡한 로직을 줄이기 위한 전략 방식 사용
@duckchanahn
Copy link
Author

프로젝트 구조

.
├── main
│   ├── java
│   │   └── org
│   │       └── ktb
│   │           └── dev
│   │               └── assignment
│   │                   ├── KtbBeDevAssignmentApplication.java
│   │                   ├── ServletInitializer.java
│   │                   ├── common
│   │                   │   └── config
│   │                   │       ├── SwaggerConfig.java
│   │                   │       └── WebConfig.java
│   │                   ├── core
│   │                   │   ├── exception
│   │                   │   │   ├── BusinessException.java
│   │                   │   │   ├── CustomErrorCode.java
│   │                   │   │   ├── CustomErrorMessage.java
│   │                   │   │   └── ErrorCode.java
│   │                   │   ├── handler
│   │                   │   │   ├── GlobalExceptionHandler.java
│   │                   │   │   └── validation
│   │                   │   │       ├── ValidationExceptionHandler.java
│   │                   │   │       └── strategy
│   │                   │   │           ├── ConstraintViolationStrategy.java
│   │                   │   │           ├── DateTimeParseStrategy.java
│   │                   │   │           ├── DefaultExceptionStrategy.java
│   │                   │   │           ├── ExceptionStrategy.java
│   │                   │   │           └── TypeMismatchStrategy.java
│   │                   │   ├── ratelimit
│   │                   │   │   ├── ApiKeyRateLimiter.java
│   │                   │   │   └── RateLimitConfig.java
│   │                   │   └── response
│   │                   │       ├── ErrorResponse.java
│   │                   │       └── SuccessResponse.java
│   │                   ├── domain
│   │                   │   ├── Company.java
│   │                   │   └── StocksHistory.java
│   │                   ├── presentation
│   │                   │   └── v1
│   │                   │       ├── controller
│   │                   │       │   ├── StockPriceApi.java
│   │                   │       │   └── StockPriceController.java
│   │                   │       └── dto
│   │                   │           ├── GetStockByCompanyCodeResponse.java
│   │                   │           └── StockPriceDto.java
│   │                   ├── repository
│   │                   │   ├── CompanyRepository.java
│   │                   │   └── StocksHistoryRepository.java
│   │                   └── service
│   │                       └── StockPriceService.java
│   └── resources
│       ├── application.yml
│       └── logback-spring.xml

@duckchanahn
Copy link
Author

코드 철학

  1. 한 클래스 내 코드는 200줄 이내로 제한하며, 단일 책임 원칙을 준수하기
  2. 중첩된 조건문은 2단계를 넘지 않도록 하여 복잡도를 관리하기
  3. 변수와 메서드 이름만으로 목적을 명확히 파악 가능하도록 작성하기

API 설계에 대한 철학

  1. RESTful API 설계를 최대한 따르기 위해 노력하기
  2. 성공과 실패 응답의 일관된 포맷 유지하기

@duckchanahn
Copy link
Author

응답 값을 설계한 이유

프론트엔드 개발을 경험하면서, API 응답이 변하면 프론트엔드 코드에서도 많은 수정이 필요하다는 점을 여러 번 경험 했습니다.
그래서 과제를 진행하며 중요하게 고려한 점은 일관된 응답 구조 유지였습니다.

✅ 성공 응답 (SuccessResponse)

@Getter
@JsonRootName("response")
@Schema(description = "성공 Response")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SuccessResponse<T> {
    private final boolean success = true;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;

    public static <T> SuccessResponse<T> of(T data) { ... }
    public static SuccessResponse<Void> ofNoData() { ... }
    public ResponseEntity<SuccessResponse<T>> asHttp(HttpStatus httpStatus) { ... }
}

✔️ 이렇게 설계한 이유

  1. 일관성 유지

    • 모든 API 응답이 success, data 형태로 제공되도록 통일.
    • 데이터를 포함할 필요가 없는 경우에도 null이 아닌 ofNoData()를 통해 빈 객체를 반환.
  2. 프론트엔드 작업 편의성

    • 클라이언트에서 응답을 파싱할 때, 항상 success 필드로 성공 여부를 확인 가능.
    • data의 존재 여부에 따라 추가적인 분기 처리가 필요 없도록 구성.
  3. HTTP 응답과 쉽게 연결

    • asHttp()를 활용해 ResponseEntity로 변환할 수 있도록 설계해 컨트롤러에서 재사용성을 높임.

❌ 실패 응답 (ErrorResponse)

@Getter
@JsonRootName("error")
@Schema(description = "실패 Response")
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ErrorResponse {
    private final boolean success = false;
    private final ErrorInfo data;

    public static ErrorResponse of(ErrorCode errorCode) { ... }
    public static ErrorResponse of(ErrorCode errorCode, List<ValidationError> validationErrorList) { ... }
    public static ErrorResponse of(ErrorCode errorCode, String message) { ... }
}

✔️ 이렇게 설계한 이유

  1. 에러 코드 표준화

    • ErrorCode를 Enum으로 관리하여 예외 케이스를 명확하게 정의.
    • 클라이언트는 code 값을 기반으로 프론트에서 적절한 메시지를 보여줄 수 있음.
  2. 검증 오류 처리

    • ValidationError 객체를 활용해 유효성 검사 실패 시 어느 필드에서 문제가 발생했는지 명확하게 제공.
    • field, message, rejectedValue를 포함하여 디버깅이 용이하도록 구성.
  3. 일관된 응답 구조

    • ErrorResponsesuccess 필드를 포함해 클라이언트에서 일관된 방식으로 성공/실패 여부를 확인 가능.
    • data 안에 code, message를 포함하여 가독성을 유지.

🎯 CustomErrorCode를 활용한 예외 관리

@RequiredArgsConstructor
public enum CustomErrorCode implements ErrorCode {
    MISSING_REQUIRED_PARAMETER(400, "E001", CustomErrorMessage.MISSING_PARAMETER),
    INVALID_PARAMETER_VALUE(400, "E002", CustomErrorMessage.INVALID_PARAMETER),
    ...
}

✔️ 이렇게 설계한 이유

  1. 에러 코드 재사용성 증가

    • 에러 메시지를 CustomErrorMessage로 분리해 필요할 때 formatMessage(args)로 동적 메시지 생성 가능.
    • 예를 들어 COMPANY_NOT_FOUND("기업 코드 {0}를 찾을 수 없습니다") 형태로 가변 데이터를 삽입 가능.
  2. 유지보수 용이

    • API 오류 코드가 여러 곳에서 사용될 경우, Enum을 활용해 중앙 집중식으로 관리 가능.
    • 필요하면 새로운 에러 코드 추가도 용이함.

@duckchanahn
Copy link
Author

로그 정책

로그 정책을 처음 세워보면서 고민이 많았고, 기존에 배포했던 경험을 바탕으로 여러 자료를 조사하여 정책을 설계했습니다.

일반 애플리케이션 로그

  • 단일 파일 최대 크기: 50MB
  • 최대 보관 크기: 5GB
  • 보관 기간: 30일
  • 압축 저장 (.gz)
    → EC2 프리티어 기준 가용 디스크 공간을 고려하여 설정

에러 로그

  • 단일 파일 최대 크기: 20MB
  • 최대 보관 크기: 2GB
  • 보관 기간: 60일
  • ERROR 레벨 로그만 저장
    → 일반 로그보다 용량을 작게 설정해 저장 공간을 확보하면서, 중요한 오류 로그는 더 오랫동안 유지

설정 이유

EC2 프리티어 환경과 자주 사용되는 로그 정책을 고려하여 설정하였습니다.

  1. 로그 총 저장공간을 7GB로 설정
    => EBS 최대 20GB 중 애플리케이션이 약 10GB 사용한다고 가정 후 가용 로그 공간을 35%로 설정

  2. 일반 로그와 에러 로그를 분리
    => 일반 로그보다 오류 로그가 중요하다고 생각해 더 길게 보관하기 위해 분리

  3. maxFileSize, totalSizeCap 설정
    => 로그가 급격히 증가할 경우에도 서비스 운영에 영향을 주지 않도록 하기 위해 설정


@duckchanahn
Copy link
Author

duckchanahn commented Feb 16, 2025

라이브러리 선택 이유

📌 Spring Boot 3.4.2 선택 이유

  • Spring Boot 3.4.2는 LTS(Long Term Support) 후보 버전 이므로 장기적인 유지보수를 고려해 안정적인 버전을 선택

📌 주요 라이브러리 및 선택 이유

1️⃣ Spring Boot Starter 라이브러리

  • spring-boot-starter-web
    • REST API 개발을 위한 기본적인 Web 기능 제공 (내장 Tomcat 포함)
  • spring-boot-starter-data-jpa
    • ORM을 활용한 데이터베이스 접근을 위해 JPA 사용
    • MySQL과 함께 사용하여 유지보수성과 확장성을 고려
  • spring-boot-starter-validation
    • Spring의 @Valid@Validated를 활용한 데이터 유효성 검증을 지원

2️⃣ 데이터베이스 및 JPA

  • com.mysql:mysql-connector-j
    • MySQL DB 사용을 위한 JDBC 드라이버
  • spring-boot-starter-data-jpa
    • Spring Data JPA를 활용하여 객체 지향적인 DB 접근 방식 구현

3️⃣ Lombok

  • org.projectlombok:lombok
    • @Getter, @Setter, @RequiredArgsConstructor 등으로 코드량을 줄이고 가독성을 높이기 위해 사용
    • annotationProcessor 추가로 IDE 및 빌드 도구에서 문제없이 처리되도록 설정

4️⃣ API 문서화 및 응답 데이터 처리

  • org.springdoc:springdoc-openapi-starter-webmvc-ui
    • Swagger UI 기반 API 문서 자동 생성
    • REST API 테스트 및 문서화를 간편하게 하기 위해 선택
  • com.fasterxml.jackson.dataformat:jackson-dataformat-xml
    • JSON뿐만 아니라 XML 응답 형식을 지원할 수 있도록 설정

5️⃣ API 성능 및 안정성 강화

  • com.google.guava:guava
    • API Throttling(요청 제한) 기능을 구현하기 위해 사용
    • Google Guava의 RateLimiter를 활용하여 특정 시간 동안 요청량을 제한할 수 있도록 함

@duckchanahn
Copy link
Author

사전과제 회고

📌 꾸준한 학습의 중요성

최근 반년간 프론트엔드 중심으로 개발을 진행하며, 백엔드는 Express.js만 사용하다 보니 익숙했던 Spring의 설정부터 개발까지 어려움을 겪었다. 특히, 기존에 Java 1.8 + Spring Boot 2.7 환경에 익숙해져 있어, 최신 버전인 Java 17 + Spring Boot 3.2.4를 사용하는 데 있어서 최신 기능을 제대로 활용하지 못했다.

몇년동안 스프링밖에 안 했는데 고작 반년 안 했다고 스프링 개발이 어색할 거라고는 상상조차 못했다.

빠른 제출보다는 Spring을 처음부터 다시 학습한다는 마음으로 접근했고, 과거 프로젝트에서 사용했던 구조를 최신 버전에 맞게 리팩토링하는 과정에 집중했다. 이 과정에서 record Pattern Matching같은 Java 17의 새로운 기능을 통해 코드의 가독성을 높일 수 있었던 거 같다.

스프링 개발도 최신 버전을 학습하는 게 이렇게 중요한지 몰랐다. 앞으로는 개발만 하는 게 아니라 공부를 많이많이 해야겠다.

시간이 부족해 더 나은 코드를 구현하지 못한 점이 아쉽지만,앞으로 더 열심히 해야하는 이유를 다시금 되새길 수 있었다.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant