1차 스코프에서 스페이스(Space) CRUD 작업을 완료하였으며, 이후 다른 CRUD 작업도 맡기로 했다.
나는 간단한 유효성 검사와 함께 채널(Channel), 프로필(Profile), 그리고 친구(Friend) 기능을 추가로 맡았다.
채널쪽은 다른사람이 일부 구현해놓은거를 내가 완성 시키는게 목적이었고 프로필은 이미 해봐서 간단했었다.
친구쪽 부분은 2차스코프였지만 매우 중요한(?) , 꼭 있어야할 시스템이기 때문에 같이 구현했다.
채널(Channel) API 설계는 다음과 같이 구성하였습니다.
채널 생성 시, 텍스트(Text) 혹은 음성(Voice) 타입(T 또는 V)을 선택해야 하며, 다른 타입이 입력되면 예외를 발생시키도록 처리했습니다. 프론트엔드에서 T와 V만 선택할 수 있게 할 수도 있지만, 백엔드 차원에서 유효성 검사를 추가한 것입니다.
채널(Channel) DTO 설계를 다음과 같이 하였습니다.
/**
* ChannelRequestDto 클래스는 채널 생성 및 업데이트 요청에 사용되는 데이터를 정의
*/
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class ChannelRequestDto {
@NotBlank(message = "채널 이름은 필수 입력 항목입니다.")
@Size(max = 50, message = "채널 이름은 50자 미만이어야 합니다.")
private String channelName;
@NotBlank(message = "채널 타입은 필수 입력 항목입니다.")
@Pattern(regexp = "^[TV]$", message = "채널 타입은 T 또는 V이어야 합니다.")
private String channelType;
}
/**
* ChannelResponseDto 클래스는 채널 응답 데이터를 정의
*/
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class ChannelResponseDto {
private Long id;
private String channelName;
private String channelType;
}
엔티티를 작성하여 enum 타입으로 T(텍스트)와 V(음성)를 지정하였습니다.
/**
* Channel 엔티티는 채널 테이블의 데이터를 매핑
*/
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "channel")
public class Channel {
@Id
private Long id;
private Long spaceId;
private String channelName;
private String channelType;
@Transient
private Space space;
@Builder
public Channel(Long id, Long spaceId, String channelName, String channelType, Space space) {
this.id = id;
this.spaceId = spaceId;
this.channelName = channelName;
this.channelType = channelType;
this.space = space;
}
public enum Type {
T, V
}
}
에러 코드와 성공 코드는 이전 블로그에서 사용한 방식과 동일하게 각 기능의 error 패키지 안에 @@ErrorCode, @@SuccessCode 형태로 관리합니다. 필요한 경우 해당 양식에 맞춰 코드를 불러와 사용합니다.
@Getter
@AllArgsConstructor
public enum ChannelErrorCode implements BaseCode {
CHANNEL_NOT_FOUND(HttpStatus.NOT_FOUND, 404, "채널 데이터를 찾을 수 없습니다.", "채널 데이터가 존재하지 않습니다."),
INVALID_CHANNEL_NAME(HttpStatus.BAD_REQUEST, 5003, "채널 이름은 50자 미만이어야 합니다.", "채널 이름이 유효하지 않습니다."),
INVALID_CHANNEL_TYPE(HttpStatus.BAD_REQUEST, 5004, "채널 타입은 T 또는 V이어야 합니다.", "채널 타입이 유효하지 않습니다.");
private final HttpStatus status;
private final int code;
private final String msg;
private final String remark;
@Override
public CommonReason getCommonReason() {
return CommonReason.builder()
.status(status)
.code(code)
.msg(msg)
.build();
}
}
/**
* ChannelSuccessCode 열거형(enum)은 채널 관련 성공 메시지를 정의
*/
@Getter
@AllArgsConstructor
public enum ChannelSuccessCode implements BaseCode {
CHANNEL_DELETE(3, "채널 삭제 완료입니다.", "채널 삭제가 성공적으로 완료되었습니다.");
private final int code;
private final String msg;
private final String remark;
@Override
public CommonReason getCommonReason() {
return CommonReason.builder()
.status(HttpStatus.OK)
.code(code)
.msg(msg)
.build();
}
}
/**
* ChannelRepository는 채널 데이터를 처리하는 리포지토리 인터페이스
*/
public interface ChannelRepository extends ReactiveCrudRepository<Channel, Long> {
Flux<Channel> findBySpaceId(Long spaceId);
}
/**
* ChannelController는 채널 관련 API 요청을 처리
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/spaces/{spaceId}/channels")
public class ChannelController {
private final ChannelFacade channelFacade;
@PostMapping
public Mono<ResponseEntity<ChannelResponseDto>> createChannel(@PathVariable Long spaceId, @Valid @RequestBody ChannelRequestDto requestDto) {
return channelFacade.createChannel(spaceId, requestDto)
.map(ResponseEntity::ok);
}
@GetMapping
public Flux<ChannelResponseDto> getChannels(@PathVariable Long spaceId) {
return channelFacade.getChannels(spaceId);
}
@PutMapping("/{channelId}")
public Mono<ResponseEntity<ChannelResponseDto>> updateChannel(@PathVariable Long channelId, @Valid @RequestBody ChannelRequestDto requestDto) {
return channelFacade.updateChannel(channelId, requestDto)
.map(ResponseEntity::ok);
}
@DeleteMapping("/{channelId}")
public Mono<ResponseEntity<String>> deleteChannel(@PathVariable Long channelId) {
return channelFacade.deleteChannel(channelId)
.then(Mono.just(ResponseEntity.ok(ChannelSuccessCode.CHANNEL_DELETE.getMsg())));
}
}
/**
* ChannelFacade는 채널 관련 비즈니스 로직을 처리하는 서비스 레이어
*/
@RequiredArgsConstructor
@Component
public class ChannelFacade {
private final ChannelService channelService;
private final SpaceService spaceService;
public Mono<ChannelResponseDto> createChannel(Long spaceId, ChannelRequestDto requestDto) {
return spaceService.findSpaceById(spaceId)
.flatMap(space -> channelService.createChannel(spaceId, requestDto));
}
public Flux<ChannelResponseDto> getChannels(Long spaceId) {
return channelService.getChannels(spaceId);
}
public Mono<ChannelResponseDto> updateChannel(Long channelId, ChannelRequestDto requestDto) {
return channelService.updateChannel(channelId, requestDto);
}
public Mono<Void> deleteChannel(Long channelId) {
return channelService.deleteChannel(channelId);
}
}
/**
* ChannelService는 채널 관련 비즈니스 로직을 처리.
*/
@RequiredArgsConstructor
@Service
public class ChannelService {
private final ChannelRepository channelRepository;
public Mono<ChannelResponseDto> createChannel(Long spaceId, ChannelRequestDto requestDto) {
Channel channel = buildChannel(null, spaceId, requestDto);
return channelRepository.save(channel)
.map(this::toChannelResponseDto);
}
public Flux<ChannelResponseDto> getChannels(Long spaceId) {
return channelRepository.findBySpaceId(spaceId)
.map(this::toChannelResponseDto);
}
public Mono<ChannelResponseDto> updateChannel(Long channelId, ChannelRequestDto requestDto) {
return findChannelById(channelId)
.flatMap(channel -> {
Channel updatedChannel = buildChannel(channelId, channel.getSpaceId(), requestDto);
return channelRepository.save(updatedChannel);
})
.map(this::toChannelResponseDto);
}
public Mono<Void> deleteChannel(Long channelId) {
return findChannelById(channelId)
.flatMap(channelRepository::delete);
}
private Mono<Channel> findChannelById(Long channelId) {
return channelRepository.findById(channelId)
.switchIfEmpty(Mono.error(new CustomException(ChannelErrorCode.CHANNEL_NOT_FOUND)));
}
private Channel buildChannel(Long channelId, Long spaceId, ChannelRequestDto requestDto) {
return Channel.builder()
.id(channelId)
.spaceId(spaceId)
.channelName(requestDto.getChannelName())
.channelType(Channel.Type.valueOf(requestDto.getChannelType()).name())
.build();
}
private ChannelResponseDto toChannelResponseDto(Channel channel) {
return ChannelResponseDto.builder()
.id(channel.getId())
.channelName(channel.getChannelName())
.channelType(channel.getChannelType())
.build();
}
}
Controller , Facade , Service 이렇게 구현을 하였다.
createChannel - 채널 생성. 먼저 스페이스가 존재하는지 확인하고, 없으면 예외 처리를, 있으면 채널을 생성합니다.
getChannels - 채널 조회. Space ID를 이용하여 채널 목록을 가져옵니다.
updateChannel - 채널 수정. findChannelById()를 호출하여 채널 ID로 조회 후, 없으면 예외 처리를 하고, 있으면 저장합니다.
deleteChannel - 채널 삭제. 조회 결과가 없으면 예외 처리를 하고, 있으면 삭제 처리합니다.
서비스 단에서 다른 레포지토리를 주입받거나 파사드에서 다른 파사드를 주입받는 실수를 반복한다.
잘 이해를 하지 못했기 때문에 , 정확히 정리 하자면
파사드(Facade): 여러 서비스(Service)를 주입받을 수 있으며, 파사드에서 다른 파사드를 주입받는 것은 불가
서비스(Service): 해당 서비스에 속하는 레포지토리(Repository)만 주입받을 수 있으며, 비슷한 성격의 레포지토리는 주입 가능
'TIL' 카테고리의 다른 글
TIL - 2024/07/30 (0) | 2024.08.05 |
---|---|
TIL - 2024/07/29 (0) | 2024.08.05 |
TIL - 2024/07/27 (0) | 2024.07.31 |
TIL - 2024/07/26 (0) | 2024.07.27 |
TIL - 2024/07/19 (0) | 2024.07.19 |