[TIL 42일 차] Sprint Mission6 - DTO 도입 ~ BinaryContent 저장 로직 고도화
오늘의 학습
- 개발 진행 상황
- DTO 도입
- Entity를 DTO로 매핑하는 Mapper 컴포넌트 정의
- Mapper 추가로 인한 Service 로직 수정
- BinaryContent 저장 로직 고도화
- 바이너리 데이터 저장만을 담당하는
BinaryContentStorage인터페이스 설계 및 구현- 바이너리 데이터 저장 메서드 :
UUID put(UUID, byte[]) - 바이너리 데이터를 읽어
InputStream타입으로 반환하는 메서드 :InputStream get(UUID) - HTTP API로 바이너리 데이터 다운로드 메서드 :
ResponoseEntity<?> download(BinaryContentDto)
- 바이너리 데이터 저장 메서드 :
- download API 구현
- 바이너리 데이터 저장만을 담당하는
- DTO 도입
- 고민: Mapper가 다른 Mapper를 재사용하고, Mapper가 재사용한 Mapper로 만들어진 Dto를 필드로 가질 때
@Mapper(componentModel = "spring", uses = BinaryContentMapper.class)이 설정 이외의 다른 설정이 필요 없을까?User안에profile필드가 있고, 그 타입이BinaryContent이고,UserDto의profile타입이BinaryContentDto이며,BinaryContentMapper에BinaryContentDto toDto(BinaryContent binaryContent)메서드가 있다면- MapStruct는
profile필드를 보고 자동으로BinaryContentMapper의 매핑 메서드를 찾아서 사용한다.
- 고민: Entity에 없는 필드가
ChannelDto에 존재하고, 직접 작성한 로직을 해당 Dto에 넣고 싶을 때,ChannelMapper는 어떻게 구성되어야 할까?@Mapping의expression옵션을 사용하면 된다.source필드를 그대로 사용하지 않고, 직접 작성한 로직을target필드에 넣고 싶을 때 사용하는 옵션이다.@Mapping(target = "lastMessageAt", expression = "java(assignLastMessageAt(channel)))")
- 더불어,
@Mapping(source = "status.isOnlineStatus", target = "online")처럼 메서드를 호출하는 식으로 작성되면 컴파일 문제가 발생할 수 있기 때문에expression옵션을 사용해@Mapping(target = "online", expression = "java(user.getStatus().isOnlineStatus())")처럼 표현
- 고민:
ChannelMapper를 만드는데 인터페이스가 아닌 추상 클래스를 사용한 이유- 요구사항에 따라 의존성 필드로
MessageRepository와ReadStatusRepository,UserMapper를 사용되어야 하고, - “의존성 필드를 이용해 직접 작성한 로직 + 자동 MapStruct 매핑”이 존재하기 때문에 인터페이스보다는 추상 클래스가 더 선호됨
- 참고로, MapStruct는 mapper를 인터페이스와 추상(abstract) 클래스로 만들 수 있다.
- 요구사항에 따라 의존성 필드로
- 추상 클래스로 작성된
ChannelMapper에서 직접 작성한 로직은protected를 접근 제어자로 가져야 한다.- MapStruct는 인터페이스로 작성된 Mapper는 해당 Mapper를 구현하는 클래스로, 추상 클래스로 작성된 Mapper는 해당 Mapper를 상속하는 클래스로 실제 구현 클래스를 생성한다.
- 그래서 상속한 자식 클래스에서 접근하기 위해서
protected를 접근 제어자로 가져야 한다.
- 문제 : “org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.sprint.mission.discodeit.entity.User#354c200e-9876-427a-ab49-b933f3a14f93]” 예외 발생
- 원인 :
BaseEntity에 UUID를 랜덤 생성하는 로직이 있어서 Spring Data JPA의save()가 id가 있는 객체로 보게 되고,persist가 아닌merge를 하게 된다. 하지만 해당 id를 지닌 객체는 db에 존재하지 않음으로 예외 발생 - 해결 : UUID를 랜덤 생성하는 로직을 지우고
@NoArgsConstructor를 추가
- 원인 :
- 문제 : “org.postgresql.util.PSQLException: 오류: “created_at” 칼럼(해당 릴레이션 “binary_contents”)의 null 값이 not null 제약조건을 위반했습니다.” 오류 발생
-
즉,
@CreatedDate가 제대로 동작하지 않아,created_at이 제때 생성되지 않음 - 원인 :
BinaryContent가 상속하고 있는BaseEntity에 이미@EntityListeners(AuditingEntityListener.class)가 있지만, auditing을 켜도록 안내하는@EnableJpaAuditing애너테이션이 존재하지 않음 - 해결 : 메인 애플리케이션 클래스에
@EnableJpaAuditing추가
-
프로젝트 요구 사항
// ...
2-6. DTO 적극 도입하기
// ...
- Entity를 DTO로 매핑하는 로직을 책임지는 Mapper 컴포넌트를 정의해 반복되는 코드를 줄여보세요.
- 패키지명:
com.sprint.mission.discodeit.mapper

- 패키지명:
2-7. BinaryContent 저장 로직 고도화
데이터베이스에 이미지와 같은 파일을 저장하면 성능 상 불리한 점이 많습니다. 따라서 실제 바이너리 데이터는 별도의 공간에 저장하고, 데이터베이스에는 바이너리 데이터에 대한 메타 정보(파일명, 크기, 유형 등)만 저장하는 것이 좋습니다.
- BinaryContent 엔티티는 파일의 메타 정보(
fileName,size,contentType)만 표현하도록bytes속성을 제거하세요. - BinaryContent의
byte[]데이터 저장을 담당하는 인터페이스를 설계하세요. 저장 매체의 확장성(로컬 저장소, 원격 저장소)을 고려해 인터페이스부터 설계합니다.- 패키지명:
com.sprint.mission.discodeit.storage -
클래스 다이어그램

- BinaryContentStorage
- 바이너리 데이터의 저장/로드를 담당하는 컴포넌트입니다.
UUID put(UUID, byte[])- UUID 키 정보를 바탕으로
byte[]데이터를 저장합니다. - UUID는 BinaryContent의 Id 입니다.
- UUID 키 정보를 바탕으로
InputStream get(UUID)- 키 정보를 바탕으로
byte[]데이터를 읽어 InputStream 타입으로 반환합니다. - UUID는 BinaryContent의 Id 입니다.
- 키 정보를 바탕으로
ResponseEntity<?> download(BinaryContentDto)- HTTP API로 다운로드 기능을 제공합니다.
- BinaryContentDto 정보를 바탕으로 파일을 다운로드할 수 있는 응답을 반환합니다.
- 패키지명:
- 서비스 레이어에서 기존에 BinaryContent를 저장하던 로직을 BinaryContentStorage를 활용하도록 리팩토링하세요.
- BinaryContentController에 파일을 다운로드하는 API를 추가하고, BinaryContentStorage에 로직을 위임하세요.
- 엔드포인트:
GET /api/binaryContents/{binaryContentId}/download - 요청
- 값: BinaryContentId
- 방식: Query Parameter
- 응답:
ResponseEntity<?> -
클래스 다이어그램

- 엔드포인트:
- 로컬 디스크 저장 방식으로 BinaryContentStorage 구현체를 구현하세요.
-
클래스 다이어그램

-
discodeit.storage.type값이local인 경우에만 Bean으로 등록되어야 합니다.Path root- 로컬 디스크의 루트 경로입니다.
discodeit.storage.local.root-path설정값을 정의하고, 이 값을 통해 주입합니다.
void init()- 루트 디렉토리를 초기화합니다.
- Bean이 생성되면 자동으로 호출되도록 합니다.
Path resolvePath(UUID)- 파일의 실제 저장 위치에 대한 규칙을 정의합니다.
- 파일 저장 위치 규칙 예시:
{root}/{UUID}
- 파일 저장 위치 규칙 예시:
put,get메소드에서 호출해 일관된 파일 경로 규칙을 유지합니다.
- 파일의 실제 저장 위치에 대한 규칙을 정의합니다.
ResponseEntity<Resource> donwload(BinaryContentDto)get메소드를 통해 파일의 바이너리 데이터를 조회합니다.- BinaryContentDto와 바이너리 데이터를 활용해
ResponseEntity<Resource>응답을 생성 후 반환합니다.
// ...
3-4. MapStruct 적용
- Entity와 DTO를 매핑하는 보일러플레이트 코드를 MapStruct 라이브러리를 활용해 간소화해보세요.
GitHub Repository 주소
https://github.com/JungH200000/10-sprint-mission/tree/sprint6
Leave a comment