🧠 AI 브레인스토밍 플랫폼 개발 일지 - Day 2

날짜: 2024년 11월 21일
진행자: AI 코칭 + 개발자
목표: ERD 설계 및 JPA 엔티티 구현


📝 오늘의 진행 사항

1️⃣ ERD 설계 (손그림 스케치)

  • ✅ 4개 테이블 구조 확정
    • User (사용자)
    • Idea (아이디어)
    • Inquiry (문의)
    • BaseEntity (공통 필드)

ERD 구조:

User (사용자)
├─ PK  userId
├─ UK  email
├─     username
├─     provider (OAuth2)
├─     providerId
├─     role
└─     createdAt, updatedAt (상속)

Idea (아이디어)
├─ PK  ideaId
├─ FK  userId → User
├─     title
├─     content (JSON)
├─     purpose
└─     createdAt, updatedAt (상속)

Inquiry (문의)
├─ PK  inquiryId
├─ FK  userId → User
├─     title
├─     content
├─     status
├─     reply
└─     createdAt, updatedAt (상속)

BaseEntity (추상 클래스)
├─     createdAt
└─     updatedAt

2️⃣ DDD 패키지 구조 생성

src/main/java/com/brainstorming/brainstorming_platform/
├── domain/
│   ├── user/
│   │   └── entity/
│   │       ├── User.java
│   │       ├── LoginProvider.java (Enum)
│   │       └── MyRole.java (Enum)
│   ├── idea/
│   │   └── entity/
│   │       └── Idea.java
│   └── inquiry/
│       └── entity/
│           ├── Inquiry.java
│           └── InquiryStatus.java (Enum)
├── global/
│   └── BaseEntity.java
└── BrainstormingPlatformApplication.java

DDD 구조 선택 이유:

  • 도메인별 응집도 높음
  • 기능 추가/삭제 용이
  • 각 도메인 독립적 관리

3️⃣ BaseEntity 구현 (공통 필드 상속)

최종 코드:

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

핵심 Annotation:

  • @MappedSuperclass: 테이블 생성 안 함, 필드만 상속
  • @EntityListeners: JPA Auditing 리스너 등록
  • @CreatedDate: 생성 시간 자동 입력
  • @LastModifiedDate: 수정 시간 자동 갱신

4️⃣ Enum 클래스 작성

LoginProvider (OAuth2 제공자):

public enum LoginProvider {
    GOOGLE, KAKAO, NAVER
}

MyRole (사용자 권한):

public enum MyRole {
    ADMIN, USER
}

InquiryStatus (문의 상태):

public enum InquiryStatus {
    PENDING,   // 답변 대기
    ANSWERED   // 답변 완료
}

5️⃣ User 엔티티 구현

최종 코드:

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    private String email;
    private String username;

    @Enumerated(EnumType.STRING)
    private LoginProvider provider;

    private String providerId;

    @Enumerated(EnumType.STRING)
    private MyRole role;
}

주요 Annotation 설명:

  • @Entity: JPA 엔티티 선언
  • @Table(name = "users"): MySQL 예약어 충돌 방지
  • @Id: PK 지정
  • @GeneratedValue(IDENTITY): Auto Increment
  • @Enumerated(EnumType.STRING): Enum을 문자열로 저장

6️⃣ Idea 엔티티 구현

최종 코드:

@Entity
@Table(name = "ideas")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Idea extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long ideaId;

    private Long userId;  // FK (나중에 연관관계 추가)

    private String title;
    private String content;   // JSON 형식으로 저장
    private String purpose;   // Q1 답변 (선택사항)
}

설계 결정:

  • status 필드 제외 (CRUD만 지원, 상태 관리 불필요)
  • content는 TEXT 타입으로 JSON 저장
  • purpose는 브레인스토밍 Q1 답변 저장용

7️⃣ Inquiry 엔티티 구현

최종 코드:

@Entity
@Table(name = "inquiries")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Inquiry extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long inquiryId;

    private Long userId;  // FK

    private String title;
    private String content;

    @Enumerated(EnumType.STRING)
    private InquiryStatus status;

    private String reply;  // 관리자 답변
}

8️⃣ JPA Auditing 활성화

Application.java 수정:

@EnableJpaAuditing  // JPA Auditing 활성화
@SpringBootApplication
public class BrainstormingPlatformApplication {
    public static void main(String[] args) {
        SpringApplication.run(BrainstormingPlatformApplication.class, args);
    }
}

Auditing 작동 원리:

1. @EnableJpaAuditing → JPA Auditing 켜기
2. @EntityListeners → 감시자 등록
3. @CreatedDate, @LastModifiedDate → 자동 시간 입력

9️⃣ H2 콘솔에서 테이블 확인

실행 결과:

-- 생성된 테이블들
USERS (user_id, email, username, provider, provider_id, role, created_at, updated_at)
IDEAS (idea_id, user_id, title, content, purpose, created_at, updated_at)
INQUIRIES (inquiry_id, user_id, title, content, status, reply, created_at, updated_at)

✅ 모든 테이블 정상 생성!
✅ BaseEntity의 createdAt, updatedAt 필드 상속 확인!


💡 주요 학습 내용

1. Entity vs DTO 개념 정리

Entity (지금 만든 것)

@Entity
@Getter  // Entity에도 Getter 필요!
public class User extends BaseEntity {
    private Long userId;
    private String email;
}

역할:

  • DB 테이블과 1:1 매핑
  • 비즈니스 로직 내부에서 사용
  • JPA가 데이터 읽을 때 Getter 필요

DTO (나중에 추가)

@Getter
@Setter
public class UserResponseDto {
    private Long userId;
    private String email;
    // 필요한 필드만!
}

역할:

  • API 요청/응답
  • Controller ↔ Service 데이터 전달
  • 외부와 통신할 때 사용

실제 흐름

1. Client → Controller: DTO (요청)
2. Controller → Service: DTO → Entity 변환
3. Service → Repository: Entity (저장)
4. Repository → Service: Entity (조회)
5. Service → Controller: Entity → DTO 변환
6. Controller → Client: DTO (응답)

왜 Entity에 @Getter가 필요한가?

  1. JPA가 데이터 읽을 때
  2. Service에서 Entity → DTO 변환할 때
  3. 로그/디버깅할 때

왜 Entity에 @Setter는 안 붙이나?

// ❌ Setter 사용 - 무분별한 변경 가능
user.setRole(MyRole.ADMIN);  // 위험!

// ✅ 비즈니스 메서드 - 검증 로직 포함
user.updateEmail(newEmail);  // 안전!

2. long vs Long

타입 설명 사용
long 기본형, null 불가 ❌ Entity에서 비권장
Long 객체형, null 가능 ✅ Entity에서 권장

이유: JPA는 null을 다뤄야 하는 경우가 많음


3. @Enumerated(EnumType.STRING)

@Enumerated(EnumType.ORDINAL)  // ❌ 숫자로 저장 (0, 1, 2)
@Enumerated(EnumType.STRING)   // ✅ 문자열로 저장 ("GOOGLE")

STRING을 써야 하는 이유:

// ORDINAL 사용 시 문제
public enum LoginProvider {
    GOOGLE,  // 0
    KAKAO,   // 1
    NAVER    // 2
}

// 나중에 순서 변경하면?
public enum LoginProvider {
    NAVER,   // 0 ← 기존 GOOGLE 데이터와 충돌!
    GOOGLE,  // 1 ← 기존 KAKAO 데이터와 충돌!
    KAKAO    // 2
}

// STRING 사용하면 안전
DB: "GOOGLE", "KAKAO", "NAVER" ← 순서 바뀌어도 OK

4. Auditing 3종 세트 (자동 시간 관리)

세트 구성

// 1. BaseEntity
@EntityListeners(AuditingEntityListener.class)
@CreatedDate
@LastModifiedDate

// 2. Application
@EnableJpaAuditing

// 3. 작동!
User user = new User(...);
userRepository.save(user);
// createdAt, updatedAt 자동 입력!

작동 원리

@EnableJpaAuditing (켜기)
    ↓
@EntityListeners (감시자 등록)
    ↓
save() 호출 감지
    ↓
@CreatedDate, @LastModifiedDate (자동 입력)

5. Lombok Annotation 정리

Annotation 생성되는 것 목적
@NoArgsConstructor 기본 생성자 JPA 필수
@AllArgsConstructor 전체 필드 생성자 테스트/편의
@Getter getter 메서드 필드 접근
@Setter setter 메서드 ⚠️ Entity에 비권장
@Builder 빌더 패턴 가독성 좋은 객체 생성

Entity에서 권장하는 조합:

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class User {
    // ...
}

🤔 의사결정 과정

Q1. BaseEntity를 왜 상속으로?

선택: extends BaseEntity
이유:

  • 모든 엔티티에 createdAt, updatedAt 필요
  • 코드 중복 제거 (DRY 원칙)
  • 자동 시간 관리 (Auditing)

비교:

// ❌ 상속 안 쓰면
public class User {
    private LocalDateTime createdAt;  // 중복
    private LocalDateTime updatedAt;  // 중복
}
public class Idea {
    private LocalDateTime createdAt;  // 중복
    private LocalDateTime updatedAt;  // 중복
}

// ✅ 상속 사용
public abstract class BaseEntity {
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
public class User extends BaseEntity {}
public class Idea extends BaseEntity {}

Q2. Idea에 status 필드 필요?

선택: 불필요 ❌
이유:

  • 브레인스토밍 → 완성된 아이디어만 저장
  • 임시 저장 기능 없음
  • 공개/비공개 구분 없음
  • CRUD만 지원

status가 필요한 경우:

  • 임시 저장 (DRAFT)
  • 승인 프로세스 (PENDING, APPROVED)
  • 공개/비공개 (PUBLISHED, PRIVATE)

Q3. InquiryStatus 어떻게?

선택: 2가지 상태 ✅

public enum InquiryStatus {
    PENDING,   // 답변 대기
    ANSWERED   // 답변 완료
}

이유:

  • 간단하고 명확
  • 확장 가능 (CLOSED 등 나중에 추가)
  • NOTREPLY → PENDING (더 명확한 네이밍)

Q4. DTO는 지금 만들어야 하나?

선택: 나중에 (Controller 만들 때) ✅
이유:

  • Entity는 DB 매핑용
  • DTO는 API 통신용
  • 단계별 학습 (지금은 Entity 집중)

개발 순서:

1. Entity 완성 (지금) ✅
2. Repository 생성 (다음)
3. Service 생성
4. Controller 생성 (이때 DTO 추가)

Q5. @Getter는 Entity에 필요?

선택: 필수! ✅
이유:

// 1. JPA가 사용
user.getEmail()

// 2. DTO 변환 시
new UserResponseDto(user.getUserId(), user.getEmail())

// 3. 비즈니스 로직
if (user.getRole() == MyRole.ADMIN) { ... }

🐛 트러블슈팅

Issue 1: BaseEntity를 필드로 포함

문제:

public class User {
    BaseEntity baseentity;  // ❌ 필드로 포함
}

원인: 상속 개념 착각
해결:

public class User extends BaseEntity {  // ✅ 상속
    // createdAt, updatedAt 자동 포함
}

교훈: Java 상속은 extends 키워드!


Issue 2: Idea의 @Id 위치 오류

문제:

@Entity
public class Idea {
    @Id  // ❌ userId가 PK?
    private Long userId;

    private Long ideaId;  // PK인데 @Id 없음!
}

원인: PK와 FK 혼동
해결:

@Entity
public class Idea {
    @Id  // ✅ ideaId가 PK
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long ideaId;

    private Long userId;  // FK
}

교훈: 각 테이블은 자신의 고유 ID(PK) 필요!


Issue 3: @Entity만 붙였는데 에러

문제: "No identifier specified for entity"
원인: @Id 없음
해결:

@Entity
public class User {
    @Id  // 필수!
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;
}

교훈: @Entity에는 반드시 @Id 필요!


Issue 4: Auditing 작동 안 함

문제: createdAt, updatedAt이 null
원인: @EnableJpaAuditing 누락
해결:

@EnableJpaAuditing  // ← 추가!
@SpringBootApplication
public class BrainstormingPlatformApplication {
}

교훈: Auditing은 3종 세트가 모두 필요!


📊 현재 상태

프로젝트 진행률: ████████░░ 30%

완료:
✅ 프로젝트 생성 (Day 1)
✅ 개발 환경 설정 (Day 1)
✅ ERD 설계 (Day 2)
✅ Entity 클래스 작성 (Day 2)
✅ JPA Annotation 설정 (Day 2)
✅ Auditing 설정 (Day 2)
✅ H2 테이블 확인 (Day 2)

진행 중:
⏳ 없음

예정:
📋 연관관계 매핑 (@ManyToOne, @OneToMany)
📋 Repository 생성
📋 간단한 CRUD 테스트

🎯 다음 할 일 (Day 3)

필수

  1. 연관관계 매핑
    • User ↔ Idea (1:N)
    • User ↔ Inquiry (1:N)
  2. @ManyToOne @JoinColumn(name = "user_id") private User user;
  3. Repository 인터페이스 생성
  4. public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); }
  5. 간단한 테스트
    • CommandLineRunner로 데이터 삽입
    • 조회 테스트

선택 (시간 되면)

  • Service 계층 구조 설계
  • 기본 CRUD 로직 작성

🔗 참고 자료


💬 회고

😊 잘한 점

  • ERD를 손으로 먼저 그려서 구조 명확화
  • DDD 패키지 구조로 확장성 확보
  • Entity 설계 시 발생한 실수들을 즉시 수정하며 학습
  • DTO vs Entity 개념 명확히 정리
  • Auditing 자동 시간 관리 적용

😅 아쉬운 점

  • 처음에 상속 개념 혼동 (필드 vs extends)
  • @Id 위치 실수 (userId vs ideaId)
  • DTO를 지금 만들어야 하나 고민

🎓 배운 점

  • 상속 활용: BaseEntity로 코드 중복 제거
  • Auditing: @CreatedDate, @LastModifiedDate로 자동 시간 관리
  • Enum 저장: EnumType.STRING이 안전한 이유
  • Entity vs DTO: 역할 구분과 사용 시점
  • long vs Long: JPA에서는 Long 권장
  • @Getter 필요성: Entity에도 Getter 필수인 이유

💡 깨달은 점

  • 단계별로 학습하는 게 이해도가 높음
  • 실수를 통해 개념이 더 명확해짐
  • 문서화(ERD)의 중요성
  • DDD 구조의 장점 (도메인별 독립성)

🚀 Day 3에서 만나요!

다음에는 연관관계 매핑과 Repository를 만들어보겠습니다! 💪

+ Recent posts