Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.runnect.server.banner.controller;

import lombok.RequiredArgsConstructor;
import org.runnect.server.banner.dto.response.GetBannerResponseDto;
import org.runnect.server.banner.service.BannerService;
import org.runnect.server.common.constant.SuccessStatus;
import org.runnect.server.common.dto.ApiResponseDto;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RestController = @Controller + @ResponseBody
메서드 리턴값을 뷰(HTML 템플릿)로 렌더링하는 대신, 그대로 JSON으로 직렬화해서 응답 바디에 실어줌. Android 쪽에서 쓰는 Retrofit 인터페이스의 반대편 — 서버가 이 JSON을 만들어서 내려주는 쪽이라고 보면 됨.

@RequiredArgsConstructor
@RequestMapping("/api/banner")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

라우팅 — @RequestMapping + @GetMapping 조합
클래스 레벨 @RequestMapping으로 공통 prefix(/api/banner)를 잡고, 메서드별로 @GetMapping/@PostMapping 등으로 세부 경로+HTTP 메서드를 정의. 여기선 메서드에 경로를 안 붙였으니 최종 엔드포인트는 GET /api/banner.

public class BannerController {

private final BannerService bannerService;

@GetMapping
@ResponseStatus(HttpStatus.OK)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ResponseStatus — 명시적 상태 코드
@RestController의 기본 성공 상태가 200이라 사실 이 줄이 없어도 동작은 같음. 이 프로젝트는 관례적으로 모든 엔드포인트에 상태 코드를 명시해둠(POST는 201 CREATED처럼 기본값과 다른 경우가 많아서, 아예 전부 명시하는 쪽으로 통일한 걸로 보임).

public ApiResponseDto<GetBannerResponseDto> getBanners() {
return ApiResponseDto.success(SuccessStatus.GET_BANNER_SUCCESS, bannerService.getBanners());

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

공통 응답 포맷(response envelope)
모든 API가 {status, success, message, data} 구조로 통일돼서 내려감. 클라이언트가 API마다 다른 응답 구조를 파싱할 필요 없이 data 안쪽만 API별로 다르게 보면 되게끔 하는 컨벤션. SuccessStatus enum이 상태코드+메시지를 한 쌍으로 관리해서 문자열이 코드 여기저기 흩어지는 걸(매직 스트링) 막아줌.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.runnect.server.banner.dto.response;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class BannerResponse {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DTO — Entity를 API 응답에 그대로 안 쓰는 이유
Entity를 그대로 내려주면 (1) DB 컬럼이 바뀔 때마다 API 스펙도 같이 바뀌고, (2) 연관관계 필드까지 직렬화되다 N+1이나 무한루프 위험이 생기고, (3) deletedAt 같은 내부 전용 필드가 노출될 수 있음. 그래서 API 응답 전용 DTO를 따로 두고, Service에서 Entity → DTO로 변환해서 내려줌.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(1) DB 컬럼 변경 = API 스펙 변경이 되는 문제

Banner 엔티티를 그대로 응답으로 내려준다고 하면, DB 컬럼명/필드명이 곧 JSON 키가 돼요. 나중에 DB 리팩터링하면서 imageUrl 컬럼명을 bannerImageUrl로 바꾼다고 해봐요 — 그 순간 API 응답의 JSON 키도 같이 바뀌어버려서, Android 앱이 아무 통보도 없이 파싱 실패함. DTO를 따로 두면 엔티티 필드명이 바뀌어도 DTO에서 매핑만 다시 해주면 되고, JSON 키(=API 계약)는 그대로 유지할 수 있어요.

// DTO가 있으면 이렇게 흡수 가능
BannerResponse.of(index, banner.getBannerImageUrl(), banner.getLinkUrl()) // 내부 필드명 바뀌어도 응답 키(imageUrl)는 안 바뀜

(2) 연관관계 직렬화 → N+1 / 무한루프

Course 엔티티를 보면 이런 연관관계가 있어요:

@OnetoOne(mappedBy = "course")
private PublicCourse publicCourse;

@onetomany(mappedBy = "course")
private List records = new ArrayList<>();

이 Course를 Jackson이 그대로 JSON으로 직렬화한다고 하면:

  • 무한루프 위험: Course → publicCourse → (PublicCourse 안에 다시 course 필드가 있다면) → 다시 Course → ... 양방향 연관관계를 계속 따라 들어가다 StackOverflowError 남
  • N+1 위험: records는 지연 로딩(fetch 기본값 LAZY)이라, 직렬화하려고 접근하는 순간 DB 쿼리가 실제로 나감. 만약 Course 여러 개를 리스트로 응답한다면 코스마다 records 조회 쿼리가 하나씩 더 나가서 쿼리 개수가 폭발함

DTO는 "정확히 이 필드들만" 골라서 담기 때문에, 응답에 필요 없는 연관관계는 애초에 건드리지도 않아요 — Jackson이 순회할 대상 자체가 없어짐.

(3) 내부 전용 필드 노출

Banner는 AuditingTimeEntity를 상속해서 deletedAt 필드를 갖고 있어요(소프트 삭제용). 엔티티를 그대로 내
{ "id": 1, "imageUrl": "...", "linkUrl": "...", "sortOrder": 0, "isActive": true, "deletedAt": null, "createdAt": "...", "updatedAt": "..." }

deletedAt이 왜 있는지, 소프트 삭제 방식을 클라이언트한테 그대로 노출돼요. 실제로배너 하나 보여주는 데 deletedAt/createdAt 같은 건 Android 입장에서 전혀 필요 없는 정보죠. DTO를 쓰면 이렇게 딱
필요한 것만 내려감:

{ "index": 0, "imageUrl": "...", "linkUrl"

한 줄 요약: Entity는 "DB와의 계약", DTO는 이 둘을 분리해두면 한쪽이 바뀌어도 다른쪽이 안전해요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무한루프 — "마주보는 두 거울"

상황: Course 객체 안에 publicCourse 필드가 있고, 그 PublicCourse 객체 안에도 다시 course라는 필드로 원래 그 Course를 가리키고 있어요. 서로가 서로를 가리키는 구조(양방향).

JSON으로 변환(직렬화)한다는 게 뭘 하는 거냐면: "이 객체의 필드를 하나씩 다 훑어서 문자열로 바꾸는" 작업이에요. Jackson이라는 라이브러리가 이걸 자동으로 해주는데, "이미 봤던 객체인지 기억하는 기능이 기본적으로 없어요. 그냥 필드를 보이는 대로 계속 따라 들어가요.

  1. Course를 JSON으로 바꾸자
  2. Course 안에 publicCourse 필드가 있네 → 이것도 JSON으로 바꿔야지
  3. publicCourse 안에 course 필드가 있네 → 이것도 JSON으로 바꿔야지
  4. 근데 그 course는 아까 그 Course잖아? → 상관없이 또 바꾸자
  5. course 안에 publicCourse 필드가 있네 → 또 바꿔야지
    ... (2번으로 다시 돌아가서 무한 반복)

이게 거울 두 개를 마주보게 놓으면 그 안의 상이 끝없이 반복되는 것과 똑같은 원리예요. 코드가 이 반복을 멈출 방법이 없어서 결국 컴퓨터가 "더 이상 못 하겠다"(StackOverflowError)며 죽어버림.

N+1 — "학생 10명 성적표를 한 명씩 따로 조회하기"

"지연 로딩(LAZY)"이 뭐냐면: Course를 DB에서 가져올 때, JPA는 그 안의 records(러닝 기록 목록)를 미리 안 가져와요. 일단 "필요하면 그때 가져올게"라는 빈 껍데기만 넣어둠. 그러다가 진짜로 course.getRecords()를 호출하는 순간, 그제서야 실제 SQL 쿼리 하나가 DB로 날아가요.

시나리오: 코스 10개를 리스트로 응답해야 한다고 해봐요.

  1. "코스 10개 가져와" → 쿼리 1번 실행 (Course 10개 조회)
  2. 이제 이 10개를 JSON으로 변환해야 함
  3. 1번째 Course의 records를 보려니 → 아직(records 조회)
  4. 2번째 Course의 records를 보려니 → 아직 안 가져왔네? → 쿼리 실행
  5. 3번째... → 쿼리 실행
    ... (10번 반복)

결과: 원래 "코스 10개 + 각자 기록"을 가져오는 데 똑똑하게 하면 쿼리 1~2번이면 될 걸, 쿼리 11번(1번 + 코스
개수만큼 N번)이 나가버려요. 이게 N+1이라 본 조회 + N개 항목마다 추가 조회). 코스가100개면 쿼리가 101개 나가서 서버가 확 느려짐.

DTO가 이 둘을 막는 방법

DTO(GetBannerResponseDto 같은 거)는 "이 필드, 저 필드"만 콕 집어서 새 객체에 담아요. Banner 엔티티 전체를 그대로 넘기는 게 아니라:

BannerResponse.of(index, banner.getImageU
이렇게 딱 3개 값만 뽑아서 새 객체를 만들 필드(publicCourse, records 같은 것)를Jackson이 쳐다볼 일 자체가 없어져요. 문제 상황(거울 마주보기, 필요할 때마다 쿼리 날리기)이 발생할 무대 자체가 없어지는 거예요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무한루프 의심 신호: mappedBy

@OnetoOne(mappedBy = "course")
private PublicCourse publicCourse;

mappedBy = "course"가 붙어있다는 건 **"나는 주인이 아니고, 저쪽(PublicCourse)에 있는 course라는 필드가 진짜 연관관계의 주인이다"**라는 뜻이에요. 이 말은 곧 — PublicCourse.java를 열어보면 반드시 Course course; 같은 필드가 있다는 걸 의미해요(안 그러면 mappedBy가 가리킬 대상이 없어서 애초에 컴파일도 안 됨).

즉 mappedBy를 보는 순간 "아, 이건 양방향이구나 → 저쪽 엔티티도 이쪽을 도로 가리키고 있겠구나" 하고 추론할 수 있어요. 양방향 = 서로 가리킴 = 직렬화하면 서로 왔다갔다 반복할 위험. 실제로 확인하고 싶으면 PublicCourse.java 열어서 course 필드가 있는지 보면 됨(memory 검증하듯).

참고로 이게 너무 흔한 문제라서, Jackson엔 아예 이 상황 전용 애너테이션(@JsonManagedReference/@JsonBackReference, 또는 그냥 @JsonIgnore)이 따로 있어요. "이런 전용 해결책이 존재한다" = "이게 흔하게 터지는 문제라는 방증"이에요.

N+1 의심 신호: @onetomany / @manytomany (컬렉션 타입)

@onetomany(mappedBy = "course")
private List records = new ArrayList<>();

여기서 신호는 타입이 List라는 것 자체예요. JPA 스펙 자체가 @OneToMany/@manytomany는 기본값이 LAZY로 정해져 있어요(반대로 @ManyToOne/@OnetoOne은 기본이 EAGER). 그래서 @onetomany 보이면 "이건 기본적으로 안 가져와져 있고, 건드리는 순간 쿼리 나간다"고 바로 가정할 수 있어요.

그리고 N+1이 "N+1"이 되는 이유는 리스트 안에 리스트가 있기 때문이에요:

  • Course 하나의 records를 조회하면 → 그냥 쿼리 +1개 (N+1까지는 아님)
  • Course **여러 개(리스트)**를 순회하면서 각자의 records를 건드리면 → 코스 개수(N)만큼 쿼리가 반복됨 → N+1

즉 패턴은: "List를 여러 개 순회하neToMany 필드를 또 건드리는가?" — 이게보이면 N+1을 의심하는 거예요.

정리 — 암기할 두 줄

  • mappedBy가 보이면 → 양방향이다 → 반대편 엔티티 확인 → 직렬화 시 순환참조 의심
  • @OneToMany/@manytomany(즉 List<...> 필 → 리스트를 순회하며 이 필드를 건드리면 N+1의심

private Integer index;
private String imageUrl;
private String linkUrl;

public static BannerResponse of(Integer index, String imageUrl, String linkUrl) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static factory method — public 생성자 대신 of()
생성자를 PRIVATE로 막고 이름 있는 정적 메서드로만 객체를 만들게 하는 관용구. new BannerResponse(1, url, url)보다 BannerResponse.of(index, imageUrl, linkUrl)가 읽기 좋고, 나중에 생성 시점에 검증/가공 로직을 추가하기도 쉬움.

return new BannerResponse(index, imageUrl, linkUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.runnect.server.banner.dto.response;

import java.util.List;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class GetBannerResponseDto {
private List<BannerResponse> banners;

public static GetBannerResponseDto of(List<BannerResponse> banners) {
return new GetBannerResponseDto(banners);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
39 changes: 39 additions & 0 deletions src/main/java/org/runnect/server/banner/entity/Banner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.runnect.server.banner.entity;

import javax.persistence.*;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.runnect.server.common.entity.AuditingTimeEntity;

@Getter
@Entity

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@entity — ORM 매핑
JPA(자바 ORM 표준)에게 "이 클래스는 DB 테이블 하나에 대응된다"고 알려주는 애너테이션. Room의 @Entity와 개념은 같은데, JPA는 필드 변경 감지(dirty checking)까지 자동화되어 있어서 트랜잭션 안에서 필드만 바꿔도(save() 명시 호출 없이) 커밋 시점에 알아서 UPDATE 쿼리가 나감.

@NoArgsConstructor(access = AccessLevel.PROTECTED)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPA는 왜 파라미터 없는 생성자가 필요할까
Hibernate가 DB row를 객체로 바꿀 때 리플렉션으로 빈 객체부터 만들고 필드를 채워 넣기 때문에 기본 생성자가 필수. 그렇다고 아무데서나 new Banner()로 빈 값 객체를 만들 수 있게 열어두긴 싫어서 PROTECTED로 제한 — JPA(같은 패키지/상속 구조)는 쓸 수 있지만 비즈니스 코드에서는 못 씀. 불변성을 지키기 위한 관용구.

public class Banner extends AuditingTimeEntity {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuditingTimeEntity — 공통 필드는 상속으로 분리
createdAt/updatedAt/deletedAt처럼 거의 모든 테이블에 필요한 필드를 @MappedSuperclass 부모 클래스로 빼서 상속받음. Spring Data JPA Auditing(@EnableJpaAuditing)이 저장/수정 시점에 자동으로 채워줘서, 매번 LocalDateTime.now()를 직접 넣을 필요가 없음.


@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PK 생성 전략(GenerationType.IDENTITY)
DB의 auto-increment(PostgreSQL SERIAL)에 ID 생성을 위임하는 전략. INSERT 쿼리를 실제로 날려야 ID 값을 알 수 있다는 특징이 있음(SEQUENCE 전략과 달리 배치 insert 최적화가 제한적). 이 프로젝트는 기존 Course 엔티티와 동일하게 IDENTITY로 통일해서 씀.

private Long id;

@Column(nullable = false)
private String imageUrl;

@Column(nullable = false)
private String linkUrl;

@Column(nullable = false)
private Integer sortOrder;

@Column(nullable = false)
private Boolean isActive;

@Builder

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빌더 패턴 — 생성자 대신 쓰는 이유
필드가 여러 개일 때 new Banner(a, b, c)처럼 순서/타입을 외워야 하는 걸 Banner.builder().imageUrl(...).linkUrl(...).build()처럼 이름을 붙여 조립하게 해줌(Lombok이 보일러플레이트 자동 생성). isActive처럼 항상 고정값(true)으로 시작해야 하는 필드는 빌더 파라미터에서 빼고 생성자 본문에서 직접 세팅.

public Banner(String imageUrl, String linkUrl, Integer sortOrder) {
this.imageUrl = imageUrl;
this.linkUrl = linkUrl;
this.sortOrder = sortOrder;
this.isActive = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.runnect.server.banner.repository;

import java.util.List;

import org.runnect.server.banner.entity.Banner;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BannerRepository extends JpaRepository<Banner, Long> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JpaRepository<Entity, PK타입> 상속만으로 얻는 것
구현체 없이 save/findById/findAll/delete 등 기본 CRUD가 바로 생김. Room의 @Dao 인터페이스가 어노테이션 프로세서로 구현체를 만들어주는 것과 비슷한 발상인데, JPA는 컴파일 타임이 아니라 런타임에 프록시로 구현체를 만들어 끼워 넣음.


List<Banner> findByIsActiveTrueOrderBySortOrderAscIdAsc();
}
30 changes: 30 additions & 0 deletions src/main/java/org/runnect/server/banner/service/BannerService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.runnect.server.banner.service;

import java.util.ArrayList;
import java.util.List;

import lombok.RequiredArgsConstructor;
import org.runnect.server.banner.dto.response.BannerResponse;
import org.runnect.server.banner.dto.response.GetBannerResponseDto;
import org.runnect.server.banner.entity.Banner;
import org.runnect.server.banner.repository.BannerRepository;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class BannerService {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service 계층 — Controller에 로직을 안 두는 이유
Controller는 HTTP 요청/응답 변환만 얇게 담당하고, 실제 비즈니스 로직(정렬/변환/여러 Repository 조합 등)은 Service에 모아둠. 이렇게 나눠두면 같은 로직을 다른 API나 배치 잡에서도 재사용하기 쉬움.


private final BannerRepository bannerRepository;

public GetBannerResponseDto getBanners() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 @Transactional이 없는 이유
단순 조회 1건이라 Spring Data JPA의 SimpleJpaRepository가 메서드 자체에 읽기 전용 트랜잭션을 자동으로 걸어줌. 여러 Repository를 조합하거나 쓰기(저장/수정)가 들어가면 그때부터 Service 메서드에 명시적으로 @Transactional을 붙여야 함 — PublicCourseService의 쓰기 메서드들에 붙어 있는 걸 참고.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

트랜잭션(세이브 포인트): 기본적으로 트랜잭션은 "여기서부터 지켜보다가, 끝나면 커밋 or 롤백"하는 거였죠. 이건 읽기든 쓰기든 다 포함해서 지켜봐요.

"읽기 전용": 그중에서 "나는 이 트랜잭션 안에서 절대 안 씀, 조회만 할 거야"라고 Spring/Hibernate한테 미리 알려주는 옵션이에요.

왜 이걸 알려주면 좋은가 — 아까 배운 dirty checking이랑 직결됨: @entity에서 말씀드렸던 거 기억나실 텐데, Hibernate는 트랜잭션 안에서 로드한 엔티티의 필드가 바뀌면 자동으로 감지해서(dirty checking) 커밋 시점에 UPDATE를 날려요. 근데 이 감지를 하려면 "엔티티를 로드했을 때의 원본 상태를 계속 기억해뒀다가, 나중에 비교하는" 작업이 필요해요 — 이게 은근히 메모리/CPU를 씀.

readOnly = true라고 미리 말해두면, Hibernate가 "아, 이 트랜잭션에선 어차피 아무것도 안 바뀔 테니 그 원본 상태 기억하고 비교하는 작업 자체를 생략해도 되겠다"고 판단해서 그 오버헤드를 통째로 스킵해요. 그리고 커밋 직전에 하는 "혹시 바뀐 거 있나 확인하고 flush"하는 과정도 생략함.

정리: 읽기 전용 트랜잭션 = "이 트랜잭션 안에서 쓰기는 안 한다"고 미리 선언해서, 프레임워크가 쓰기 대비용 부가 작업(변경 감지, flush)을 안 하게 만드는 최적화 힌트예요. BannerService.getBanners()는 실제로 SELECT만 하니까 이 최적화를 100% 안전하게 받을 수 있는 케이스인 거고요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPA: 자바 진영에서 정한 표준 명세(interface 모음) — @entity, @id, EntityManager 같은 애너테이션/인터페이스만 정의해놓은 거고, 실제로 동작하는 코드는 하나도 없음.

Hibernate: 그 JPA 표준을 실제로 구현한 라이브러리. @entity가 붙은 클래스를 보고 실제 SELECT/INSERT/UPDATE SQL을 만들어서 DB에 날리는 진짜 일꾼.

List<Banner> banners = bannerRepository.findByIsActiveTrueOrderBySortOrderAscIdAsc();

List<BannerResponse> bannerResponses = new ArrayList<>();
for (int index = 0; index < banners.size(); index++) {
Banner banner = banners.get(index);
bannerResponses.add(BannerResponse.of(index, banner.getImageUrl(), banner.getLinkUrl()));
}

return GetBannerResponseDto.of(bannerResponses);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum SuccessStatus {

GET_HEALTH_DATA_SUCCESS(HttpStatus.OK, "건강 데이터 조회 성공"),
GET_HEALTH_SUMMARY_SUCCESS(HttpStatus.OK, "건강 통계 조회 성공"),
GET_BANNER_SUCCESS(HttpStatus.OK, "배너 조회 성공"),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enum으로 성공 상태를 관리하는 이유
API마다 성공 메시지를 하드코딩하면 오타나 중복이 나기 쉬움. SuccessStatus(HttpStatus, message) 쌍으로 몰아두면 이 서버의 모든 성공 응답 목록을 한 곳에서 파악할 수 있음. 에러도 동일한 패턴으로 ErrorStatus enum이 따로 있음.



UPDATE_RECORD_SUCCESS(HttpStatus.OK, "활동 기록 수정 성공"),
Expand Down
Loading