ERD ์ค๊ณ ๋จ๊ณ์์ Enum Category ๋ฅผ ์ํฐํฐํ ํ์ฌ ๋ฐ๋ก Category ํ
์ด๋ธ๋ก ๋ถ๋ฆฌํ์ฌ ๊ด๋ฆฌํ๊ธฐ๋ก ๊ฒฐ์ . (๋ฃฉ์
ํ
์ด๋ธ)
Enum์ผ๋ก ๊ด๋ฆฌํ๋ ๊ฒ์ DB ํ
์ด๋ธ์ ์ ์ฅํจ์ผ๋ก์จ ์ ์ฐํ๊ฒ ๊ด๋ฆฌํ๊ณ , ์ฐ๊ด๊ด๊ฒ ์ค์ ์ฉ์ดํด์งโก๏ธ์์์ฒ๋ผ ์ฌ์ฉํ ๊ฒ์ด๋ฏ๋ก ๋ฏธ๋ฆฌ ์ด๊ธฐ๊ฐ์ ์ธํ ํ์ฌ ๋ฃ์ด์ค ํ์๊ฐ ์์.
๊ทธ๋ ๋ค๋ฉด ์ด๊ธฐํ๋ ์ด๋ป๊ฒ, ์ด๋์ ํด์ค ๊ฒ์ธ๊ฐ?
1๏ธโฃDB ์ด๊ธฐํ ๋จ๊ณ์ sql ๋ฌธ ๋ฃ๊ธฐ
enum๊ณผ ์ฝ๋ ๋ถ์ผ์น ๊ฐ๋ฅ์ฑ2๏ธโฃโ ์ดํ๋ฆฌ์ผ์ด์ ์คํ ์์ ์ ์ด๊ธฐํํ๋๋ก ์ฝ๋์ ์์ฑ
2๋ฒ ๋ฐฉ์์ ์ ํ
Enum ๊ฐ์ด ์ฝ๋์ ์ ์๋์ด ์์ผ๋ฏ๋ก ๊ด๋ฆฌ ์ฉ์ด@PostConstruct ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ์ฌ ์ด๊ธฐํ
@PostConstruct ์ด๋
ธํ
์ด์
@Profile ์ด๋
ธํ
์ด์
์ผ๋ก ๊ฐ๋ฐ ํ๊ฒฝ์ ๋ง๋ application.properties ํ์ผ ์ค์ ์ ํ ํ์๊ฐ ์์.โก๏ธ@Component+@PreConstuct ์กฐํฉํ์ฌ CategoryInitializer ์ปดํฌ๋ํธ ์์ฑ
@Component
@RequiredArgsConstructor
class CategoryInitializer {
private final PostCategoryRepository postCategoryRepository;
// ์คํ๋ง ์ดํ๋ฆฌ์ผ์ด์
์คํ ์ ๋จ ํ๋ฒ ์คํํ์ฌ ์นดํ
๊ณ ๋ฆฌ DB์ ์ด๊ธฐ๊ฐ ์์ฑ
@PostConstruct
public void initCategories() {
if(postCategoryRepository.count() == 0) {
for(CategoryType type:CategoryType.values()) {
// ์ค๋ณต ์ ์ฅ์ด ์๋๋ผ๋ฉด
if(!postCategoryRepository.existsByCategoryType(type)) {
postCategoryRepository.save(new Category(type));
}
}
}
}
}
enum๋ณด๋ค ๋์ ํ์ฅ์ฑ๊ณผ ์ ์ฐ์ฑenum์ ์ถ๊ฐํ๋ฉด ์๋์ผ๋ก DB์๋ ๋ฐ์๋จ (์ดํ๋ฆฌ์ผ์ด์
์คํ ์ค๋จ ํ์)| ๊ธฐ๋ฅ | Enum๋ง ์ฌ์ฉ | Entity ์ฌ์ฉ |
|---|---|---|
| DB์์ JOIN | โ ์ ๋จ | โ ๊ฐ๋ฅ |
Category ์ด๋ฆ ์ธ์ ๋ถ๊ฐ ์ ๋ณด ์ฌ์ฉ (e.g. displayName) | โ ๋ถ๊ฐ | โ ๊ฐ๋ฅ |
Category ํต๊ณ ์ฟผ๋ฆฌ (e.g. GROUP BY) | โ ์ด๋ ต๊ณ ๋นํจ์จ์ | โ ๊น๋ํ๊ฒ ๊ฐ๋ฅ |
| Category ID๋ก ์ฐธ์กฐ (Foreign Key) | โ ๋ถ๊ฐ | โ ๊ฐ๋ฅ |
| JPA ๊ด๊ณ (FK ๋งคํ) | โ ์์ | โ
@ManyToOne ๋ฑ์ผ๋ก ๋งคํ ๊ฐ๋ฅ |
| ์ถํ ์นดํ ๊ณ ๋ฆฌ ํ์ฅ์ฑ (e.g. ์นดํ ๊ณ ๋ฆฌ ์ค๋ช ์ถ๊ฐ ๋ฑ) | โ ์ด๋ ต๊ฑฐ๋ Enum ๋ณ๊ฒฝ ํ์ | โ ์ ์ฐํ๊ฒ ์ปฌ๋ผ ์ถ๊ฐ ๊ฐ๋ฅ |
โ ๋ฐ์ดํฐ ๊ฐ ๊ด๊ณ๊ฐ(์ฐ๊ด ๊ด๊ณ) ํ์ํ๊ฑฐ๋ ํ์ฅ์ด ์์๋๋ฉด Entity๋ก ๊ด๋ฆฌํ๋ ๊ฒ์ด ํจ์ฌ ์ ๋ฆฌ
is_deleted --> deleted_at (NULL/Timestamp)์ผ๋ก ๋ณ๊ฒฝid ๋ ์ฌ์ค์ ์ฐ๋ ๊ณณ์ด ์์ผ๋ ๋ณตํฉํค(์ฌ์ฉ์, ๋๊ธ ์๋ณ์)๋ก ์ฌ์ฉํ๋ ๊ฑธ ์ถ์ฒpath variable์ ๋ณดํต ์๋ณ์๊ฐ ๋ ์ ์๋ ์ ๋ํฌ๊ฐ์ผ๋ก ์ฌ์ฉ@Entity
public class Post extends BaseEntity{
...
private List<String> imgUrls;
}
List์ ๊ฐ์ ์ปจํ
์ด๋ ํ์
์ ๊ทธ๋๋ก JPA์ ์ ์ฅ์ด ๋์ง ์๋๋ค.ListToStringConverter๋ฅผ ํตํด String์ผ๋ก ๋ณํ ํ ์ ์ฅํด์ผํจ.@Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(List<String> dataList) {
try {
return mapper.writeValueAsString(dataList);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public List<String> convertToEntityAttribute(String data) {
try {
return mapper.readValue(data, List.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
convertToDatabaseColumn : List๋ฅผ DB์ ์ ์ฅ๋ ์ ์๋๋ก String์ผ๋ก ๋ณํ.
List.of("a", "b", "c") โ "["a","b","c"]"convertToEntityAttribute : DB์ ์ ์ฅ๋ String์ List์ผ๋ก ๋ณํ.
"["a","b","c"]" โ List.of("a", "b", "c")@Convert(converter = StringListConverter.class)
private List<String> imgUrls; // ์ด๋ฏธ์ง URLs
@Convert ์ด๋
ธํ
์ด์
๊ณผ converter๋ฅผ ์ง์ ํด์ฃผ๋ฉด DB์ ์๋ํ๋๋ก ์ ์ฅ๋๋ค.@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Visibility visibility;
@Enumberated ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ์ฌ enum ํ์
์ ์ด๋ป๊ฒ DB์ ์ ์ฅํ ์ง ์ง์ ํ ์ ์๋ค.
EnumType.STRING : enum์ ์ด๋ฆ(string)์ผ๋ก ์ ์ฅ."SUNNY", "CLOUDY", "RAINY", "STORMY"EnumType.ORDINAL : enum์ ์์๊ฐ(int)์ผ๋ก ์ ์ฅ.SUNNY โ 0, CLOUDY โ 1, RAINY โ 2, STORMY โ 3์ด๋ค ๊ฐ์ธ์ง, ์ด๋ค ์ํ๋ฅผ ์๋ฏธํ๋์ง ํ์
ํ๊ธฐ ์ฝ๋๋ก EnumType.STRING์ ์ฌ์ฉํ์ฌ ์ ์ฅํ๋๋ก ํ์๋ค.
Java์์๋ ๊ธฐ๋ณธ ํ์ ๋ค์ ๋ํ ๋ค์ํ Validation ์ด๋ ธํ ์ด์ ์ ์ ๊ณตํ์ง๋ง, Enum ํ์ ์ ๋ํด์๋ ๊ธฐ๋ณธ ์ ๊ณต๋๋ Validation ์ด๋ ธํ ์ด์ ์ด ์๋ค.
json ๋งคํ ์ JsonMappingException ๋๋ IllegalArgumentException์ด ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก, ์ ์ ํ Validation์ด ํ์.๋ฐฉ๋ฒ1๏ธโฃ
๋ฐฉ๋ฒ2๏ธโฃ
โ
์ปค์คํ
validation ์ด๋
ธํ
์ด์
์ฌ๋ฌ ๋๋ฉ์ธ์์ Enum ๊ฐ ๊ฒ์ฆ์ด ํ์ํ ์ ์์ผ๋ฏ๋ก ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฒ์ฉ ์ด๋ ธํ ์ด์ ์ผ๋ก ๊ตฌํํจ.
๊ธฐ์กด์๋ DTO์์ Enum ํ์
์ผ๋ก ์ง์ ๋ฐ์ ์ฌ์ฉํ์ง๋ง, ํ์ฑ ์ค๋ฅ(JsonParsingException ๋ฑ) ๊ฐ ๋ฐ์ํ ์ ์์ด Validation ๋จ๊ณ๊น์ง ๋๋ฌํ์ง ๋ชปํ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ ํ ์ ์์.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ์์ฒญ์ String์ผ๋ก ๋ฐ๊ณ , ๊ฒ์ฆ ํ Entity ๋ณํ ์ Enum์ผ๋ก ๋งคํํ๋ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝํจ.
@Target({ElementType.FIELD, ElementType.PARAMETER}) // ๊ฒ์ฆ ๋์ (ํ๋, ํ๋ผ๋ฏธํฐ)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EnumValidator.class, EnumListValidator.class}) // ๊ฒ์ฆ ๊ตฌํ์ฒด
public @interface ValidEnum {
String message() default "์ง์ํ๋ ๊ฐ์ [{enumValues}] ์
๋๋ค.";
Class<?>[] groups() default {}; // ์ ํจ์ฑ ๊ฒ์ฌ ์ ์ฉ ๋์ ๊ตฌ๋ถํ ๋ ์ฌ์ฉ
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> target(); // ๊ฒ์ฆ ๋์ enum ์ค์
boolean ignoreCase() default true; // ๋์๋ฌธ์ ๋ฌด์ ์ฌ๋ถ
}
@Target : ์ ์ฉ ๋์ ์ง์ (ํ๋, ํ๋ผ๋ฏธํฐ)@Constraint : ํด๋น ์ด๋
ธํ
์ด์
์ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง์ ๊ตฌํํ ํด๋์ค๋ฅผ ๋ช
์ (ConstraintValidator<์ด๋
ธํ
์ด์
, ํ์
> ๊ตฌํ์ฒด)public class EnumValidator implements ConstraintValidator<ValidEnum, String> {
private Set<String> allowedValues; // enum ๋ด ์ ํจ๊ฐ
private String messageTemplate;
// ๊ฒ์ฆ ์คํ ์ ์ฒ์ ํ๋ฒ๋ง ์คํ
// parameter : ์ด๋
ธํ
์ด์
์ผ๋ก ๋ค์ด์ค๋ ๋ฉํ์ ๋ณด
@Override
public void initialize(ValidEnum constraintAnnotation) {
Enum<?>[] enumValues = constraintAnnotation.target().getEnumConstants(); // enum ๋ด ์ ์๋ ์์ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
allowedValues = Arrays.stream(enumValues).map(Enum::name).collect(Collectors.toSet());
messageTemplate = convertMessageTemplate(constraintAnnotation.message());
}
// ๋จ์ผ Enum ์ ํจ๊ฐ ๊ฒ์ฆ ๋ฉ์๋
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if(value == null) {
return true;
}
String convertedValue = checkIgnoreCase(value); // ๋์๋ฌธ์ ๋ฌด์
// ์ ํจ๊ฐ ๊ฒ์ฆ
if(allowedValues.contains(convertedValue)) {
return true;
}
// ์ปค์คํ
๋ฉ์ธ์ง
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext.buildConstraintViolationWithTemplate(messageTemplate)
.addConstraintViolation();
return false;
}
// ๋์๋ฌธ์ ๋ฌด์
private String checkIgnoreCase(String value) {
return value.toUpperCase();
}
// ์ปค์คํ
๋ฉ์์ง
private String convertMessageTemplate(String message) {
return message.replace("{enumValues}", String.join(" | ", allowedValues));
}
}
initialize : ํ๋๊ฐ ์ด๊ธฐํisValid : ๊ฒ์ฆ ์ํ ๋ฉ์๋checkIgnoreCase : ์ ํ. ๋์๋ฌธ์๋ฅผ ๋ฌด์ํ๋ ๋ฉ์๋.convertMessageTemplate : ์ ํ. ์ปค์คํ
๋ฉ์ธ์ง ์ถ๋ ฅ ๋ฉ์๋.@ValidEnum(target = Visibility.class)
private final String visibility;
์ ์ฉ ๋์ ํ๋๋ ํ๋ผ๋ฏธํฐ์ ์ด๋
ธํ
์ด์
์ ๋ถ์ฌ ๊ฒ์ฆ์ ์ํํ๋ค.
PATCHโก๏ธDELETE๋ก ๋ณ๊ฒฝํ์๋ค.PATCH๋ฅด ์ฌ์ฉํ์์.DELETE๋ก ์์ ํ์์.categories == null, categories.isEmpty ๋ ๋ค ์ฌ์ฉํ์ฌ ๊ฒ์ฌcategories == null : ์์ฒญ ํ๋ผ๋ฏธํฐ๊ฐ ์์ ์ ์์ ๋categories.isEmpty : ๋น ๊ฐ์ผ๋ก ์์ ๋/posts : null/posts?categories= : ๋น ๋ฆฌ์คํธ ([])๊ธฐ์กด์๋๋ ๊ฒ์๊ธ(Post)๊ณผ ์นดํ
๊ณ ๋ฆฌ(Category) ์ฌ์ด๋ฅผ 1:N ๋จ๋ฐฉํฅ ์ฐ๊ด ๊ด๊ณ๋ก ๋งคํํ์ผ๋, ํ
์คํธ ์ค ๊ฒ์๊ธ์ ์ฌ๋ฌ ๊ฐ ์์ฑํ ์ ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์.
์ด๋ ๊ด๊ณ ์ค์ ์ ํ๊ณ๋ก, ์๋ฅผ ๋ค์ด ํ๊ณผ ๋ฉค๋ฒ ๊ด๊ณ์ฒ๋ผ:
@ManyToMany๋ก ๋ณ๊ฒฝํ์ฌ ํด๊ฒฐ.๊ทธ๋ฌ๋ ์ฌ์ ํ ํ์
์์ @ManyToMany๋ ์ ์ฌ์ฉํ์ง ์๋๋ค.
@ManyToMany์ ๋ฌธ์ ์ @OneToMany, @ManyToOne์ผ๋ก ํ์ด๋ด๊ณ ์กฐ์ธ ํ
์ด๋ธ์ ์ํฐํฐ๋ก ์น๊ฒฉํ๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ ๊ถ์ฅ.
public interface PostRepository extends JpaRepository<Post, Long> {
// ๋ชจ๋ ์ ์ฒด ๊ณต๊ฐ ๊ฒ์๊ธ์ ์์ฑ์ผ ์์ผ๋ก ์กฐํ
List<Post> findAllByVisibilityOrderByCreatedAtDesc(Visibility visibility);
// ๋ชจ๋ ์น๊ตฌ ๊ณต๊ฐ ๊ฒ์๊ธ์ ์์ฑ์ผ ์์ผ๋ก ์กฐํ
List<Post> findAllByVisibilityAndUserInOrderByCreatedAtDesc(Visibility visibility, List<User> friends);
// ์ฌ์ฉ์์ ๋ชจ๋ ์ ์ฒด ๊ณต๊ฐ ๊ฒ์๊ธ์ ์์ฑ์ผ ์์ผ๋ก ์กฐํ
List<Post> findAllByUserAndVisibilityOrderByCreatedAtDesc(User user, Visibility visibility);
// ์ ์ ์ ๋ชจ๋ ๊ฒ์๊ธ์ ์์ฑ์ผ ์์ผ๋ก ์กฐํ
List<Post> findAllByUserOrderByCreatedAtDesc(User user);
// ๊ณต๊ฐ ๋ฒ์์ ์นดํ
๊ณ ๋ฆฌ ๋ณ๋ก ์กฐํ
List<Post> findAllByVisibilityAndCategoryIdsIn(Visibility visibility, List<Category> categoryList);
}
๊ฒ์ ์กฐ๊ฑด์ด ๋ณต์กํด์ง๋ค๋ณด๋ JPA ๋ฉ์๋ ๋ช ๋ ๊ธธ์ด์ ธ์ ์คํ๋ ค ๊ฐ๋ ์ฑ์ด ์ ํ๋จ.
@Query ์ด๋
ธํ
์ด์
์ผ๋ก ์ง์ ์ฟผ๋ฆฌ๋ฌธ์ ์์ฑํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝ.
// ๋ชจ๋ ์ ์ฒด ๊ณต๊ฐ ๊ฒ์๊ธ์ ์์ฑ์ผ ์์ผ๋ก ์กฐํ
@Query("SELECT p FROM Post p WHERE p.visibility = :visibility ORDER BY p.createdAt DESC")
List<Post> findAllByVisibilityOrderByCreatedAtDesc(@Param("visibility") Visibility visibility);
@Param("ํ๋ผ๋ฏธํฐ๋ช
") ๋ฌ์์ค์ผํ๊ณ ์ธ ๋๋ :ํ๋ผ๋ฏธํฐ๋ช
์ด๋ ๊ฒ ๋ฃ์ด์ค์ผ ํจ.@OneToMany์์ @ManyToMany๋ก ๋ณ๊ฒฝ ํ DB์์ ์นดํ
๊ณ ๋ฆฌ ๊ฐ์ ์ ๋๋ก ์กฐํํ ์ ์๋ ๋ฌธ์ ๋ฐ์.FetchType.LAZY ๋ก ์ค์ ํ ๋ฌธ์ ์ธ์ง, ๋ค์ํ ์์ธ์ ์ฐพ์๋ดค์ผ๋ ๊ฒฐ๊ตญddl-auto: create ์ค์ ์ผ๋ก ๋งค ์คํ ์ DB๋ฅผ ์๋ก ์์ฑํ๋๋ก ํ์ง๋ง, ๋ค๋ฅธ ์ค์ ๋๋ ์บ์ ๋ฑ์ผ๋ก ์ธํด ๋ณ๊ฒฝ์ด ์ ์์ ์ผ๋ก ์ ์ฉ๋์ง ์์๋ ๊ฒ์ผ๋ก ์ถ์ .@PostConstruct
ListToStringConverter
ManyToMany ์ฃผ์์
PostConstruct ์ด ๋ ์ฐธ ์ ๊ธฐํ๋ค์^^
ํ ๋ฒ ๊ณต๋ถํด๋ณด๊ฒ ์ต๋๋ค ๊ฐ์ฌํฉ๋๋ค^^