// 사용자 정의 에러
RESERVATION_INVALID_ID(HttpStatus.BAD_REQUEST, "R1", "The reservation requested is invalid. Please review your request.");
private final HttpStatus status;
private final String code;
private final String msg;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({
CustomException.class
})
public ResponseEntity<ErrorResponse> handle(CustomException e) {
return new ResponseEntity<>(ErrorResponse.of(e.getErrorCode()), e.getErrorCode().getStatus());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handle(Exception e) {
return new ResponseEntity<>(ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR), ErrorCode.INTERNAL_SERVER_ERROR.getStatus());
}
}
서비스 로직에서 Entity를 반환하여 모든 정보가 드러나는 것은 보안상 위험하다. 따라서 엔티티마다 필요한 정보만을 반환할 수 있게 DTO(Data Transfer Object)로 변환하는 작업이 필요하다. 일반적으로 of, to 메서드를 만들어 변환하지만, 코드를 직접 만들어야 하는 번거로움이 있다.
MapStruct를 사용하면 필드 이름이 같다면 자동으로 매핑을 해준다. 다른 엔티티에 접근해야 할 때도 간단한 어노테이션으로 쉽게 매핑할 수 있다. 또한, 컴파일 시간에 타입 안정성을 보장하고 매핑 코드를 만들어주므로 성능상 이점이 있다. 필요한 의존성은 다음과 같이 주입한다.
implementation("org.mapstruct:mapstruct:1.5.5.Final")
@Mapper(componentModel = "spring") // 매퍼 구현체를 스프링 빈으로 등록한다
public interface ReservationMapper {
@Mapping(source = "space.spaceType", target = "spaceType")
@Mapping(source = "space.spaceName", target = "spaceName")
@Mapping(source = "space.openingTime", target = "openingTime")
@Mapping(source = "space.closingTime", target = "closingTime")
@Mapping(source = "space.hourlyRate", target = "hourlyRate")
@Mapping(source = "space.spaceSize", target = "spaceSize")
@Mapping(source = "space.maxCapacity", target = "maxCapacity")
@Mapping(source = "space.detailedTypes", target = "detailedTypes")
@Mapping(source = "space.realEstate.floor", target = "floor")
@Mapping(source = "space.realEstate.hasParking", target = "hasParking")
@Mapping(source = "space.realEstate.hasElevator", target = "hasElevator")
ResponseReservation ReservationToResponseReservation(SpaceReservation reservation);
List<ResponseReservation> ReservationsToResponseReservations(List<SpaceReservation> reservations); // List 변환까지 가능하다
}
java {
sourceCompatibility = JavaVersion.VERSION_17 // 열거형 지원
}
configurations {
val compileOnly by getting {
extendsFrom(configurations.getByName("annotationProcessor"))
}
}
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
- 공간 예약 시스템에서 공간은 기본 타입(친목, 교육, 운동, ...)을 가지며 기본 타입에 해당하는 세부 타입을 가진다. 예를 들어 교육 타입에는 스터디룸, 강의룸, 세미나룸, 미팅룸을 가질 수 있다.
- mysql SET을 이용하여 세부 사항을 저장하면 된다고 생각했다.
CREATE TABLE RoomTypes (
id INT AUTO_INCREMENT PRIMARY KEY,
room_type SET('party_room', 'residence', 'cafe',
'performance_venue', 'conference_hall', 'exhibition_hall',
'study_room', 'lecture_room', 'seminar_room', 'meeting_room',
'dance_room', 'vocal_room', 'instrument_room', 'drawing_room', 'craft_room',
'badminton_court', 'futsal_court', 'tennis_court',
'film_studio', 'broadcast_room')
);
@Column(nullable = false)
private Set<DetailedType> detailedTypes = new HashSet<>();
public enum DetailedType {
PARTY_ROOM, RESIDENCE, CAFE,
PERFORMANCE_VENUE, CONFERENCE_HALL, EXHIBITION_HALL,
STUDY_ROOM, LECTURE_ROOM, SEMINAR_ROOM, MEETING_ROOM,
DANCE_ROOM, VOCAL_ROOM, INSTRUMENT_ROOM, DRAWING_ROOM, CRAFT_ROOM,
BADMINTON_COURT, FUTSAL_COURT, TENNIS_COURT,
FILM_STUDIO, BROADCAST_ROOM;
}
org.hibernate.type.SerializationException: could not deserialize
…
Caused by: java.io.StreamCorruptedException: invalid stream header
@ElementCollection(fetch = FetchType.LAZY, targetClass = DetailedType.class)
@CollectionTable(name = "space_detailed_type", joinColumns = @JoinColumn(name = "space_id"))
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Set<DetailedType> detailedTypes = new HashSet<>();
@Query("SELECT s FROM Space s " +
"WHERE s.spaceType = :spaceType " +
"AND s.realEstate.address.sido = :sido " +
"AND s.realEstate.address.sigungu = :sigungu " +
"AND s.maxCapacity >= :minCapacity " +
"AND (:detailedType IS NULL OR :detailedType MEMBER OF s.detailedTypes) " +
"AND s.isDeleted = false")
Page<Space> findByCriteria(@Param("spaceType") SpaceType spaceType,
@Param("sido") String sido,
@Param("sigungu") String sigungu,
@Param("minCapacity") int minCapacity,
@Param("detailedType") DetailedType detailedType,
Pageable pageable);
AND (:detailedTypes IS NULL OR s.detailedTypes IN :detailedTypes)
처럼 쿼리를 작성할 수 있지 않을까?단일 IN 복수
형태로 사용되어야 하고, 위의 예시에선 복수 IN 복수
형태로 사용되기에 제대로 동작하지 않는다.@Override
public Page<Space> findByCriteriaQuerydsl(SpaceType spaceType, String sido, String sigungu,
int minCapacity, Set<DetailedType> detailedTypes, Pageable pageable) {
BooleanExpression conditions = space.spaceType.eq(spaceType)
.and(space.realEstate.address.sido.eq(sido))
.and(space.realEstate.address.sigungu.eq(sigungu))
.and(space.maxCapacity.goe(minCapacity))
.and(space.isDeleted.isFalse());
if (detailedTypes != null && !detailedTypes.isEmpty()) {
conditions = conditions.and(space.detailedTypes.any().in(detailedTypes));
}
List<Space> spaces = jpaQueryFactory.selectFrom(space)
.where(conditions)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
...
private static final Map<String, List<String>> districtsAndDongs = new HashMap<>();
districtsAndDongs.put("강남구", Arrays.asList("역삼동", "개포동", "청담동", "삼성동", "대치동", "신사동", "논현동", "압구정동", "세곡동", "자곡동", "율현동", "일원동", "수서동", "도곡동"));
districtsAndDongs.put("강동구", Arrays.asList("명일동", "고덕동", "상일동", "길동", "둔춘동", "암사동", "성내동", "천호동", "강일동"));
districtsAndDongs.put("강북구", Arrays.asList("미아동", "번동", "수유동", "우이동"));
districtsAndDongs.put("강서구", Arrays.asList("염창동", "등촌동", "화곡동", "가양동", "마곡동", "내발산동", "외발산동", "공항동", "방화동"));
districtsAndDongs.put("관악구", Arrays.asList("봉천동", "신림동", "남현동"));
...
private static String getRandomDistrict(Set<String> districts) {
Random random = new Random();
List<String> districtList = new ArrayList<>(districts);
return districtList.get(random.nextInt(districtList.size()));
}
private static String getRandomDong(List<String> list) {
Random random = new Random();
return list.get(random.nextInt(list.size()));
}
private static final Map<SpaceType, Set<DetailedType>> validDetailedTypesMap = Map.of(
SpaceType.FRIENDSHIP, EnumSet.of(DetailedType.PARTY_ROOM, DetailedType.RESIDENCE, DetailedType.CAFE),
SpaceType.EVENT, EnumSet.of(DetailedType.PERFORMANCE_VENUE, DetailedType.CONFERENCE_HALL, DetailedType.EXHIBITION_HALL),
SpaceType.EDUCATION, EnumSet.of(DetailedType.STUDY_ROOM, DetailedType.LECTURE_ROOM, DetailedType.SEMINAR_ROOM, DetailedType.MEETING_ROOM),
SpaceType.ART, EnumSet.of(DetailedType.DANCE_ROOM, DetailedType.VOCAL_ROOM, DetailedType.INSTRUMENT_ROOM, DetailedType.DRAWING_ROOM, DetailedType.CRAFT_ROOM),
SpaceType.SPORT, EnumSet.of(DetailedType.BADMINTON_COURT, DetailedType.FUTSAL_COURT, DetailedType.TENNIS_COURT),
SpaceType.PHOTOGRAPHY, EnumSet.of(DetailedType.FILM_STUDIO, DetailedType.BROADCAST_ROOM) );
private static SpaceType getRandomSpaceType () {
Random random = new Random();
SpaceType[] values = SpaceType.values();
return values[random.nextInt(values.length)];
}
private static DetailedType getRandomDetailedType(Set<DetailedType> set) {
int index = new Random().nextInt(set.size());
Iterator<DetailedType> iter = set.iterator();
for (int i = 0; i < index; i++) {
iter.next();
}
return iter.next();
}
JPA ORM 표준 프로그래밍이라는 책에서 단방향 매핑보다는 양방향 매핑 사용을 권한다. 그 이유는 @OneToMany 관계에서 외래 키가 다른 테이블에 존재하고, 연관 관계 처리를 위한 update sql이 추가로 필요하기 때문이다. 또한 성능 문제가 있다.
다대일 단방향 관계
로 구성되었다.