[TIL] ๐Ÿ› ๏ธ๋‰ด์Šคํ”ผ๋“œ ํ”„๋กœ์ ํŠธ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

YJinยท2025๋…„ 4์›” 7์ผ

[๋‚ด๋ฐฐ์บ  Spring 6๊ธฐ_TIL]

๋ชฉ๋ก ๋ณด๊ธฐ
20/56

ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

Category ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ ์ „๋žต

๋ฌธ์ œ

ERD ์„ค๊ณ„ ๋‹จ๊ณ„์—์„œ Enum Category ๋ฅผ ์—”ํ‹ฐํ‹ฐํ™” ํ•˜์—ฌ ๋”ฐ๋กœ Category ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๊ธฐ๋กœ ๊ฒฐ์ •. (๋ฃฉ์—… ํ…Œ์ด๋ธ”)

  • ๋ฃฉ์—… ํ…Œ์ด๋ธ” ์ด๋ž€?
    • ๋ฏธ๋ฆฌ ์ •์˜๋œ ๊ฐ’๋“ค์„ ํ…Œ์ด๋ธ”๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹
    • ๊ฐ ์—”ํ‹ฐํ‹ฐ์—์„œ ์ฐธ์กฐํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ์ •์  ๋ฐ์ดํ„ฐ
    • Enum์œผ๋กœ ๊ด€๋ฆฌํ•˜๋˜ ๊ฒƒ์„ DB ํ…Œ์ด๋ธ”์— ์ €์žฅํ•จ์œผ๋กœ์จ ์œ ์—ฐํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ณ , ์—ฐ๊ด€๊ด€๊ฒŒ ์„ค์ • ์šฉ์ดํ•ด์ง

  • ๋ณ€๋™์ด ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ
  • ๋ฏธ๋ฆฌ ์ •์˜๋œ ๊ฐ’๋“ค
  • ์ž์ฃผ ์ฐธ์กฐํ•จ

โžก๏ธ์ƒ์ˆ˜์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ฏ€๋กœ ๋ฏธ๋ฆฌ ์ดˆ๊ธฐ๊ฐ’์„ ์„ธํŒ…ํ•˜์—ฌ ๋„ฃ์–ด์ค„ ํ•„์š”๊ฐ€ ์žˆ์Œ.

๊ทธ๋ ‡๋‹ค๋ฉด ์ดˆ๊ธฐํ™”๋Š” ์–ด๋–ป๊ฒŒ, ์–ด๋””์„œ ํ•ด์ค„ ๊ฒƒ์ธ๊ฐ€?

ํ•ด๊ฒฐ

1๏ธโƒฃDB ์ดˆ๊ธฐํ™” ๋‹จ๊ณ„์— sql ๋ฌธ ๋„ฃ๊ธฐ

  • ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด(์ค‘๊ฐ„์— ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์ค‘๋‹จ ์—†์ด) DB ์‚ฝ์ž… ๋งŒ์œผ๋กœ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
  • enum๊ณผ ์ฝ”๋“œ ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ

2๏ธโƒฃโœ…์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์‹œ์ ์— ์ดˆ๊ธฐํ™”ํ•˜๋„๋ก ์ฝ”๋“œ์— ์ž‘์„ฑ

2๋ฒˆ ๋ฐฉ์‹์„ ์„ ํƒ

  • Enum ๊ฐ’์ด ์ฝ”๋“œ์— ์ •์˜๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ๊ด€๋ฆฌ ์šฉ์ด
  • ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ ์‹œ, ๊ฐ’ ๊ฒ€์ฆ ํŽธ๋ฆฌ (DB ์กฐํšŒ ์—†์ด)
  • ์ฝ”๋“œ ๋‚ด์—์„œ๋„ ์ƒ์ˆ˜์ฒ˜๋Ÿผ ์ง๊ด€์ ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

์ดˆ๊ธฐํ™” ๋ฐฉ๋ฒ•

@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์—๋„ ๋ฐ˜์˜๋จ (์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์ค‘๋‹จ ํ•„์š”)
  • DB์™€ ์ฝ”๋“œ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ ํ•ด๊ฒฐ

Enum์˜ ์—”ํ‹ฐํ‹ฐํ™”

Enum๋งŒ ์‚ฌ์šฉํ•  ๋•Œ vs Entity๋กœ ์‚ฌ์šฉํ•  ๋•Œ

๊ธฐ๋ŠฅEnum๋งŒ ์‚ฌ์šฉEntity ์‚ฌ์šฉ
DB์—์„œ JOINโŒ ์•ˆ ๋จโœ… ๊ฐ€๋Šฅ
Category ์ด๋ฆ„ ์™ธ์˜ ๋ถ€๊ฐ€ ์ •๋ณด ์‚ฌ์šฉ (e.g. displayName)โŒ ๋ถˆ๊ฐ€โœ… ๊ฐ€๋Šฅ
Category ํ†ต๊ณ„ ์ฟผ๋ฆฌ (e.g. GROUP BY)โŒ ์–ด๋ ต๊ณ  ๋น„ํšจ์œจ์ โœ… ๊น”๋”ํ•˜๊ฒŒ ๊ฐ€๋Šฅ
Category ID๋กœ ์ฐธ์กฐ (Foreign Key)โŒ ๋ถˆ๊ฐ€โœ… ๊ฐ€๋Šฅ
JPA ๊ด€๊ณ„ (FK ๋งคํ•‘)โŒ ์—†์Œโœ… @ManyToOne ๋“ฑ์œผ๋กœ ๋งคํ•‘ ๊ฐ€๋Šฅ
์ถ”ํ›„ ์นดํ…Œ๊ณ ๋ฆฌ ํ™•์žฅ์„ฑ (e.g. ์นดํ…Œ๊ณ ๋ฆฌ ์„ค๋ช… ์ถ”๊ฐ€ ๋“ฑ)โŒ ์–ด๋ ต๊ฑฐ๋‚˜ Enum ๋ณ€๊ฒฝ ํ•„์š”โœ… ์œ ์—ฐํ•˜๊ฒŒ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

โœ…๋ฐ์ดํ„ฐ ๊ฐ„ ๊ด€๊ณ„๊ฐ€(์—ฐ๊ด€ ๊ด€๊ณ„) ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ํ™•์žฅ์ด ์˜ˆ์ƒ๋˜๋ฉด Entity๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ์œ ๋ฆฌ

ERD ์„ค๊ณ„

Image

ํ”ผ๋“œ๋ฐฑ

  • ์—ฌ๋Ÿฌ๊ฐœ๋ฉด ๋ณต์ˆ˜ s ๋ถ™์—ฌ์ฃผ๊ธฐ
  • is_deleted --> deleted_at (NULL/Timestamp)์œผ๋กœ ๋ณ€๊ฒฝ
    • ์–ธ์ œ ํƒˆํ‡ดํ–ˆ๋Š”์ง€, ๋” ๋งŽ์€ ์ •๋ณด๋ฅผ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Œ
  • ๊ฒŒ์‹œ๊ธ€/๋Œ“๊ธ€ ์ข‹์•„์š”: id ๋Š” ์‚ฌ์‹ค์ƒ ์“ฐ๋Š” ๊ณณ์ด ์—†์œผ๋‹ˆ ๋ณตํ•ฉํ‚ค(์‚ฌ์šฉ์ž, ๋Œ“๊ธ€ ์‹๋ณ„์ž)๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฑธ ์ถ”์ฒœ
  • ๋ณตํ•ฉํ‚ค(์‚ฌ์šฉ์ž, ๋Œ“๊ธ€ ์‹๋ณ„์ž)๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์กฐํšŒํ•˜๋Š” ์ผ์ด ๋งŽ์„ ํ…๋ฐ ๊ทธ๊ฑธ๋กœ ํ•˜๋ฉด ์กฐํšŒ ์„ฑ๋Šฅ์ด ๋” ์ข‹์„ ๊ฒƒ.

API ์„ค๊ณ„

  • path variable์€ ๋ณดํ†ต ์‹๋ณ„์ž๊ฐ€ ๋  ์ˆ˜ ์žˆ๋Š” ์œ ๋‹ˆํฌ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ
  • ์„ฑ๊ณต ๋ฉ”์„ธ์ง€๋Š” ํฌ๊ฒŒ ์˜๋ฏธ๊ฐ€ ์—†๋‹ค๋ฉด ๋ณด๋‚ด์ง€ ์•Š์•„๋„ ๋œ๋‹ค. ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๊ฐ€ ๋” ์ค‘์š”
  • ์‚ฌ์šฉ์ž ์กฐํšŒ ์‹œ ์œ ์ € ๋„ค์ž„๋ณด๋‹ค๋Š” ์ž๋™์ƒ์„ฑ ID (PK) ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฆฌ์†Œ์Šค ์กฐํšŒํ•˜๋Š”๊ฒŒ ๋” ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.
    • ๋ฆฌ์†Œ์Šค ์‹๋ณ„ ๋ช…ํ™•ํ•จ
    • ์‹ ๋ขฐ์„ฑ ์žˆ์Œ
  • PK ๊ธฐ๋ฐ˜ ์กฐํšŒ๊ฐ€ DB ์„ฑ๋Šฅ ์ธก๋ฉด์—์„œ๋„ ๋” ๋น ๋ฆ„

Github Branch ์ „๋žต

  • ์ด์Šˆ ๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜ ๋ธŒ๋žœ์น˜ ๊ด€๋ฆฌ
    • ์ถ”์  ๋ฐ ๊ตฌ๋ถ„ ํŽธ๋ฆฌ
  • PR์€ ์ตœ์†Œ 2๋ช… ์Šน์ธ, ๋ฆฌ๋ทฐ์–ด๋Š” ํŒ€์› ์ค‘ ๊ทธ๋•Œ๊ทธ๋•Œ ์‹œ๊ฐ„ ๋‚˜๋Š” ์‚ฌ๋žŒ

List โžก๏ธ String Converter

@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
  • Converter ์ž‘์„ฑ ํ›„ ์ ์šฉํ•  ์—”ํ‹ฐํ‹ฐ์˜ ๋ฆฌ์ŠคํŠธ ํ•„๋“œ์— @Convert ์–ด๋…ธํ…Œ์ด์…˜๊ณผ converter๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด DB์— ์˜๋„ํ•œ๋Œ€๋กœ ์ €์žฅ๋œ๋‹ค.


JPA enum ์ €์žฅ๋ฐฉ๋ฒ•

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Visibility visibility;

@Enumberated ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ enum ํƒ€์ž…์„ ์–ด๋–ป๊ฒŒ DB์— ์ €์žฅํ•  ์ง€ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • EnumType.STRING : enum์˜ ์ด๋ฆ„(string)์œผ๋กœ ์ €์žฅ.
    • Enum WEATHER {SUNNY, CLOUDY, RAINY, STORMY}
    • ์ €์žฅ๊ฐ’: "SUNNY", "CLOUDY", "RAINY", "STORMY"

  • EnumType.ORDINAL : enum์˜ ์ˆœ์„œ๊ฐ’(int)์œผ๋กœ ์ €์žฅ.
    • Enum WEATHER {SUNNY, CLOUDY, RAINY, STORMY}
    • ์ €์žฅ๊ฐ’: SUNNY โ†’ 0, CLOUDY โ†’ 1, RAINY โ†’ 2, STORMY โ†’ 3

์–ด๋–ค ๊ฐ’์ธ์ง€, ์–ด๋–ค ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•˜๋Š”์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์‰ฝ๋„๋ก EnumType.STRING์„ ์‚ฌ์šฉํ•˜์—ฌ ์ €์žฅํ•˜๋„๋ก ํ•˜์˜€๋‹ค.



Enum validation ์–ด๋…ธํ…Œ์ด์…˜

Java์—์„œ๋Š” ๊ธฐ๋ณธ ํƒ€์ž…๋“ค์— ๋Œ€ํ•œ ๋‹ค์–‘ํ•œ Validation ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ณตํ•˜์ง€๋งŒ, Enum ํƒ€์ž…์— ๋Œ€ํ•ด์„œ๋Š” ๊ธฐ๋ณธ ์ œ๊ณต๋˜๋Š” Validation ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋‹ค.

  • ์š”์ฒญ ๋ฐ”๋””์—์„œ Enum ํƒ€์ž…์„ ์ œ๋Œ€๋กœ ๊ฒ€์ฆํ•˜์ง€ ์•Š์œผ๋ฉด json ๋งคํ•‘ ์‹œ JsonMappingException ๋˜๋Š” IllegalArgumentException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ ์ ˆํ•œ Validation์ด ํ•„์š”.

๋ฐฉ๋ฒ•1๏ธโƒฃ

  • ์ปจํŠธ๋กค๋Ÿฌ์— ๊ฒ€์ฆ ๋ฉ”์†Œ๋“œ ๊ตฌํ˜„

๋ฐฉ๋ฒ•2๏ธโƒฃ

  • โœ… ์ปค์Šคํ…€ validation ์–ด๋…ธํ…Œ์ด์…˜

  • ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ Enum ๊ฐ’ ๊ฒ€์ฆ์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฒ”์šฉ ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๊ตฌํ˜„ํ•จ.

  • ๊ธฐ์กด์—๋Š” DTO์—์„œ Enum ํƒ€์ž…์œผ๋กœ ์ง์ ‘ ๋ฐ›์•„ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, ํŒŒ์‹ฑ ์˜ค๋ฅ˜(JsonParsingException ๋“ฑ) ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด Validation ๋‹จ๊ณ„๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ๋ชปํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ ํ•  ์ˆ˜ ์žˆ์Œ.

  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, ์š”์ฒญ์€ String์œผ๋กœ ๋ฐ›๊ณ , ๊ฒ€์ฆ ํ›„ Entity ๋ณ€ํ™˜ ์‹œ Enum์œผ๋กœ ๋งคํ•‘ํ•˜๋Š” ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ•จ.


@ValidEnum

@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<์–ด๋…ธํ…Œ์ด์…˜, ํƒ€์ž…> ๊ตฌํ˜„์ฒด)


EnumValidator

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));
  }
}
  • ๋‹จ์ผ String ํ•„๋“œ์— ๋Œ€ํ•˜์—ฌ ๊ฒ€์ฆ ์ˆ˜ํ–‰
  • initialize : ํ•„๋“œ๊ฐ’ ์ดˆ๊ธฐํ™”
  • isValid : ๊ฒ€์ฆ ์ˆ˜ํ–‰ ๋ฉ”์†Œ๋“œ
  • checkIgnoreCase : ์„ ํƒ. ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๋ฌด์‹œํ•˜๋Š” ๋ฉ”์†Œ๋“œ.
  • convertMessageTemplate : ์„ ํƒ. ์ปค์Šคํ…€ ๋ฉ”์„ธ์ง€ ์ถœ๋ ฅ ๋ฉ”์†Œ๋“œ.

์ ์šฉ

@ValidEnum(target = Visibility.class)
private final String visibility;

์ ์šฉ ๋Œ€์ƒ ํ•„๋“œ๋‚˜ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.


RESTful API ์„ค๊ณ„

  • ์นœ๊ตฌ ์‚ญ์ œ API ์˜ HTTP ๋ฉ”์†Œ๋“œ Method๋ฅผ PATCHโžก๏ธDELETE๋กœ ๋ณ€๊ฒฝํ•˜์˜€๋‹ค.
  • ๋‚ด๋ถ€ ๋กœ์ง์œผ๋กœ๋Š” ์นœ๊ตฌ ๊ด€๊ณ„๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ, ์นœ๊ตฌ ์š”์ฒญ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ PATCH๋ฅด ์‚ฌ์šฉํ•˜์˜€์Œ.
  • ๊ทธ๋Ÿฌ๋‚˜ ๋‚ด๋ถ€ ๋กœ์ง ๋ณด๋‹ค๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๊ด€์ ์—์„œ ๋ฉ”์†Œ๋“œ๋ฅผ ์ •ํ•˜๋Š” ๊ฒƒ์ด๋ฏ€๋กœ, (ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋‚ด๋ถ€ ๋™์ž‘์„ ์•Œํ•„์š” ์—†์œผ๋‹ˆ) DELETE๋กœ ์ˆ˜์ •ํ•˜์˜€์Œ.


@RequestParam

  • ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์ค‘ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์žˆ์„ ์‹œ
  • categories == null, categories.isEmpty ๋‘˜ ๋‹ค ์‚ฌ์šฉํ•˜์—ฌ ๊ฒ€์‚ฌ
  • categories == null : ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์•„์˜ˆ ์•ˆ ์™”์„ ๋•Œ
  • categories.isEmpty : ๋นˆ ๊ฐ’์œผ๋กœ ์™”์„ ๋•Œ

์˜ˆ์‹œ

  • /posts : null
  • /posts?categories= : ๋นˆ ๋ฆฌ์ŠคํŠธ ([])


๊ฒŒ์‹œ๊ธ€-์นดํ…Œ๊ณ ๋ฆฌ ์—ฐ๊ด€๊ด€๊ณ„ ์„ค๊ณ„ ๋ณ€๊ฒฝ

  • ๊ธฐ์กด์—๋Š”๋Š” ๊ฒŒ์‹œ๊ธ€(Post)๊ณผ ์นดํ…Œ๊ณ ๋ฆฌ(Category) ์‚ฌ์ด๋ฅผ 1:N ๋‹จ๋ฐฉํ–ฅ ์—ฐ๊ด€ ๊ด€๊ณ„๋กœ ๋งคํ•‘ํ–ˆ์œผ๋‚˜, ํ…Œ์ŠคํŠธ ์ค‘ ๊ฒŒ์‹œ๊ธ€์„ ์—ฌ๋Ÿฌ ๊ฐœ ์ƒ์„ฑํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ.

  • ์ด๋Š” ๊ด€๊ณ„ ์„ค์ •์˜ ํ•œ๊ณ„๋กœ, ์˜ˆ๋ฅผ ๋“ค์–ด ํŒ€๊ณผ ๋ฉค๋ฒ„ ๊ด€๊ณ„์ฒ˜๋Ÿผ:

    • ๋ฉค๋ฒ„๊ฐ€ ํ•˜๋‚˜์˜ ํŒ€์—๋งŒ ์†ํ•œ๋‹ค๋ฉด 1:N์œผ๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ,
    • ๋ฉค๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ ํŒ€์— ์†Œ์†๋  ์ˆ˜ ์žˆ๋‹ค๋ฉด N:M ๋กœ ์—ฐ๊ด€๊ด€๊ณ„๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ.
  • ๊ฒŒ์‹œ๊ธ€-์นดํ…Œ๊ณ ๋ฆฌ๋„ ์ด์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ
    • ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€์ด ์—ฌ๋Ÿฌ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๊ณ 
    • ํ•˜๋‚˜์˜ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์—ฌ๋Ÿฌ๊ฐœ์˜ ๊ฒŒ์‹œ๊ธ€์— ์†Œ์†๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ @ManyToMany๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ํ•ด๊ฒฐ.

๊ทธ๋Ÿฌ๋‚˜ ์—ฌ์ „ํžˆ ํ˜„์—…์—์„  @ManyToMany๋Š” ์ž˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

@ManyToMany์˜ ๋ฌธ์ œ์ 

  • ์กฐ์ธ ํ…Œ์ด๋ธ” ํ™œ์šฉ ๋ถˆ๊ฐ€
    • JPA์—์„œ @ManyToMany ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์กฐ์ธ ํ…Œ์ด๋ธ”์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑ
    • ๊ทธ๋Ÿฌ๋‚˜ ์ถ”ํ›„ ๊ธฐ๋Šฅ ํ™•์žฅ ์‹œ ์กฐ์ธ ํ…Œ์ด๋ธ”์„ ํ™œ์šฉํ•˜์—ฌ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ๋„ฃ๊ฑฐ๋‚˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ ์šฉํ•  ๋•Œ ํ™•์žฅ์„ฑ์ด ๋–จ์–ด์ง„๋‹ค๋Š” ๋‹จ์  ์กด์žฌ
  • ์„ฑ๋Šฅ ์ด์Šˆ
    • ์ž๋™์œผ๋กœ ์กฐ์ธ์ด ์—ฐ์‡„์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์„ฑ๋Šฅ ์ €ํ•˜
  • ๋”ฐ๋ผ์„œ, @OneToMany, @ManyToOne์œผ๋กœ ํ’€์–ด๋‚ด๊ณ  ์กฐ์ธ ํ…Œ์ด๋ธ”์„ ์—”ํ‹ฐํ‹ฐ๋กœ ์Šน๊ฒฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ.
  • ์ถ”ํ›„ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ด ๋ฐฉ๋ฒ•์„ ์ ์šฉํ•ด์•ผ๊ฒ ๋‹ค.


@Query ์‚ฌ์šฉ


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 ๋กœ ์„ค์ •ํ•œ ๋ฌธ์ œ์ธ์ง€, ๋‹ค์–‘ํ•œ ์›์ธ์„ ์ฐพ์•„๋ดค์œผ๋‚˜ ๊ฒฐ๊ตญ
  • ์ค‘๊ฐ„์— ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์ด DB์— ์ œ๋Œ€๋กœ ๋ฐ˜์˜์ด ์•ˆ๋œ ๊ฒƒ์ด ๋ฌธ์ œ์˜€๋‹ค.
  • ddl-auto: create ์„ค์ •์œผ๋กœ ๋งค ์‹คํ–‰ ์‹œ DB๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜๋„๋ก ํ–ˆ์ง€๋งŒ, ๋‹ค๋ฅธ ์„ค์ • ๋˜๋Š” ์บ์‹œ ๋“ฑ์œผ๋กœ ์ธํ•ด ๋ณ€๊ฒฝ์ด ์ •์ƒ์ ์œผ๋กœ ์ ์šฉ๋˜์ง€ ์•Š์•˜๋˜ ๊ฒƒ์œผ๋กœ ์ถ”์ •.
  • ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๋ณ€๊ฒฝํ•œ ๊ฒฝ์šฐ์—๋Š” ๊ธฐ์กด DB๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•˜๋‹ค.


์ฐธ๊ณ 

@PostConstruct

ListToStringConverter

ManyToMany ์ฃผ์˜์ 

profile
๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ๋„ ๋ฝ์ด๋‹ค

2๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2025๋…„ 4์›” 15์ผ

PostConstruct ์ด ๋†ˆ ์ฐธ ์‹ ๊ธฐํ•˜๋„ค์š”^^
ํ•œ ๋ฒˆ ๊ณต๋ถ€ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค^^

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
2025๋…„ 4์›” 15์ผ

PostConstruct ์ด ๋†ˆ ์ฐธ ์‹ ๊ธฐํ•˜๋„ค์š”^^
ํ•œ ๋ฒˆ ๊ณต๋ถ€ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค^^

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ