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

날짜: 2024년 11월 21일 (저녁)
진행자: 개발자 + AI 코칭
목표: Repository 및 Service 계층 구현


📝 오늘의 진행 사항

🎯 총 작업량

  • Repository 3개 작성 (30분)
  • Service 3개 작성 (1시간)
  • 코드 리뷰 및 개념 학습 (1시간 30분)

총 소요 시간: 약 3시간


1️⃣ Repository 계층 구현

📁 생성한 파일

domain/
├── user/repository/UserRepository.java
├── idea/repository/IdeaRepository.java
└── inquiry/repository/InquiryRepository.java

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {

    // 이메일로 사용자 찾기 (OAuth 로그인용)
    Optional<User> findByEmail(String email);

    // 이메일 존재 여부 확인 (중복 체크용)
    boolean existsByEmail(String email);
}

핵심 메서드:

  • findByEmail(): OAuth 로그인 시 기존 사용자 확인
  • existsByEmail(): 회원가입 시 중복 체크

IdeaRepository

public interface IdeaRepository extends JpaRepository<Idea, Long> {

    // 특정 사용자의 모든 아이디어 조회
    List<Idea> findByUserId(Long userId);

    // 특정 사용자의 아이디어 개수
    long countByUserId(Long userId);
}

핵심 메서드:

  • findByUserId(): 마이페이지에서 내 아이디어 목록 표시
  • countByUserId(): 아이디어 개수 표시

InquiryRepository

public interface InquiryRepository extends JpaRepository<Inquiry, Long> {

    // 특정 사용자의 문의 목록
    List<Inquiry> findByUserId(Long userId);

    // 상태별 문의 목록 (관리자용)
    List<Inquiry> findByStatus(InquiryStatus status);

    // 특정 사용자의 상태별 문의
    List<Inquiry> findByUserIdAndStatus(Long userId, InquiryStatus status);
}

핵심 메서드:

  • findByUserId(): 내 문의 목록
  • findByStatus(): 관리자가 미답변 문의 확인
  • findByUserIdAndStatus(): 사용자별 필터링

2️⃣ Service 계층 구현

📁 생성한 파일

domain/
├── user/service/UserService.java
├── idea/service/IdeaService.java
└── inquiry/service/InquiryService.java

IdeaService (5개 메서드)

@Service
@RequiredArgsConstructor
public class IdeaService {

    private final IdeaRepository ideaRepository;

    /**
     * 아이디어 저장
     */
    public Idea save(Idea idea) {
        return ideaRepository.save(idea);
    }

    /**
     * ID로 아이디어 조회
     */
    public Idea findById(Long ideaId) {
        return ideaRepository.findById(ideaId)
                .orElseThrow(() -> new RuntimeException("아이디어를 찾을 수 없습니다."));
    }

    /**
     * 특정 사용자의 모든 아이디어 조회
     */
    public List<Idea> findByUserId(Long userId) {
        return ideaRepository.findByUserId(userId);
    }

    /**
     * 아이디어 삭제
     */
    public void delete(Long ideaId) {
        ideaRepository.deleteById(ideaId);
    }

    /**
     * 사용자의 아이디어 개수
     */
    public long countByUserId(Long userId) {
        return ideaRepository.countByUserId(userId);
    }
}

InquiryService (4개 메서드)

@Service
@RequiredArgsConstructor
public class InquiryService {

    private final InquiryRepository inquiryRepository;

    /**
     * 문의사항 저장
     */
    public Inquiry save(Inquiry inquiry) {
        return inquiryRepository.save(inquiry);
    }

    /**
     * ID로 문의사항 조회
     */
    public Inquiry findById(Long inquiryId) {
        return inquiryRepository.findById(inquiryId)
                .orElseThrow(() -> new RuntimeException("문의사항을 찾을 수 없습니다."));
    }

    /**
     * 특정 사용자의 모든 문의 조회
     */
    public List<Inquiry> findByUserId(Long userId) {
        return inquiryRepository.findByUserId(userId);
    }

    /**
     * 문의사항 삭제
     */
    public void delete(Long inquiryId) {
        inquiryRepository.deleteById(inquiryId);
    }
}

UserService (4개 메서드)

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    /**
     * 사용자 저장 (OAuth 자동 가입용)
     */
    public User save(User user) {
        return userRepository.save(user);
    }

    /**
     * ID로 사용자 조회
     */
    public User findById(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
    }

    /**
     * 이메일로 사용자 조회
     */
    public Optional<User> findByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    /**
     * 사용자 삭제 (회원 탈퇴)
     */
    public void delete(Long userId) {
        userRepository.deleteById(userId);
    }
}

💡 주요 학습 내용

1. Repository 인터페이스 자동 구현

Spring Data JPA의 마법:

// 이것만 작성하면
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

// 이런 메서드들이 자동 생성!
save(user)           // INSERT / UPDATE
findById(1L)         // SELECT * WHERE id = 1
findAll()            // SELECT *
delete(user)         // DELETE
count()              // COUNT(*)
existsById(1L)       // 존재 여부
findByEmail(email)   // SELECT * WHERE email = ?

Query Method 규칙:

  • findBy + 필드명: 조회
  • countBy + 필드명: 개수
  • existsBy + 필드명: 존재 여부
  • deleteBy + 필드명: 삭제

2. Service 계층의 역할

Repository vs Service:

Repository Service
DB 접근 비즈니스 로직
CRUD 자동 생성 검증, 변환, 조합
인터페이스만 구현 필요

Service 패턴:

Service 메서드 = Repository 메서드 + 비즈니스 로직

// 간단한 경우
public Idea save(Idea idea) {
    return repository.save(idea);
}

// 복잡한 경우 (나중에)
public Idea save(Idea idea) {
    // 1. 검증
    validate(idea);
    // 2. 저장
    Idea saved = repository.save(idea);
    // 3. 알림 등 추가 작업
    notifyUser(saved);
    return saved;
}

3. Optional 사용 전략

언제 예외? 언제 Optional?

예외 처리 (orElseThrow):

// 없으면 시스템 오류
public User findById(Long userId) {
    return userRepository.findById(userId)
        .orElseThrow(() -> new RuntimeException("사용자 없음"));
}

사용 시나리오:

  • 마이페이지 조회 (사용자 반드시 존재)
  • 아이디어 상세 (아이디어 반드시 존재)
  • 권한 확인 (사용자 반드시 존재)

Optional 반환:

// 없어도 정상 흐름
public Optional<User> findByEmail(String email) {
    return userRepository.findByEmail(email);
}

사용 시나리오:

  • OAuth 로그인 (첫 로그인 시 없음)
  • 이메일 중복 체크 (없는 게 정상)
  • 검색 기능 (검색 결과 없을 수 있음)

패턴 비교:

// 패턴 1: 없으면 예외
public User findById(Long id) {
    return repository.findById(id)
        .orElseThrow(() -> new RuntimeException("Not Found"));
}

// 패턴 2: Optional 반환
public Optional<User> findByEmail(String email) {
    return repository.findByEmail(email);
}

// 패턴 3: 기본값 제공
public User findByIdOrDefault(Long id) {
    return repository.findById(id)
        .orElse(createGuestUser());
}

4. Primary Key vs Foreign Key 차이

PK (Primary Key) - ideaId

// PK로 조회 → 1개만!
Idea idea = ideaService.findById(1L);

// DB:
idea_id | title
--------|-------
1       | "AI 앱"  ← 딱 1개만 존재!

특징:

  • 유일 (UNIQUE)
  • 중복 불가
  • 조회하면 0개 또는 1개

FK (Foreign Key) - userId

// FK로 조회 → 여러 개!
List<Idea> ideas = ideaService.findByUserId(100L);

// DB:
idea_id | user_id | title
--------|---------|-------
1       | 100     | "AI 앱"
2       | 100     | "챗봇"    ← 같은 userId
3       | 100     | "게임"    ← 여러 개 가능!

특징:

  • 중복 가능
  • 여러 개 허용
  • 조회하면 0개 이상

5. @RequiredArgsConstructor 이해

Lombok이 자동 생성:

@RequiredArgsConstructor
public class IdeaService {
    private final IdeaRepository ideaRepository;
}

// 실제 생성되는 코드:
public class IdeaService {
    private final IdeaRepository ideaRepository;

    public IdeaService(IdeaRepository ideaRepository) {
        this.ideaRepository = ideaRepository;
    }
}

장점:

  • 생성자 자동 생성
  • 의존성 주입 자동 처리
  • 코드 간결화

🤔 의사결정 과정

Q1. Repository 메서드는 최소한으로?

결정: 필요한 것만 추가 ✅

이유:

  • YAGNI (You Aren't Gonna Need It)
  • 나중에 필요하면 추가
  • 미리 만들면 유지보수 부담

예시:

// 처음
List<Idea> findByUserId(Long userId);

// 나중에 필요하면
List<Idea> findByUserIdAndTitle(Long userId, String title);
Page<Idea> findByUserId(Long userId, Pageable pageable);

Q2. UserService에 save() 필요?

결정: 필요! ✅

이유:

  1. OAuth 자동 회원가입
  2. 테스트 데이터 생성
  3. 관리자 직접 생성

OAuth 로그인 시나리오:

public User handleOAuthLogin(OAuth2User oauth2User) {
    String email = oauth2User.getAttribute("email");

    // 기존 사용자 확인
    Optional<User> existing = userService.findByEmail(email);

    if (existing.isPresent()) {
        return existing.get();
    } else {
        // 첫 로그인 → 자동 회원가입
        User newUser = new User(...);
        return userService.save(newUser);  // save 필요!
    }
}

Q3. findById vs findByEmail 패턴 차이?

결정: 비즈니스 의미에 따라 다르게 ✅

findById - 예외 처리:

public User findById(Long userId) {
    return userRepository.findById(userId)
        .orElseThrow(...);
}
  • 의미: "반드시 있어야 함"
  • 없으면: 시스템 오류

findByEmail - Optional 반환:

public Optional<User> findByEmail(String email) {
    return userRepository.findByEmail(email);
}
  • 의미: "있을 수도, 없을 수도"
  • 없음: 정상 흐름

🐛 트러블슈팅

Issue 1: findById 파라미터명 혼동

문제:

public Idea findById(Long userId) {  // ❌ userId?

원인: 복붙하면서 파라미터명 수정 안 함

해결:

public Idea findById(Long ideaId) {  // ✅ ideaId

교훈: 파라미터명은 명확하게!

  • ideaId for Idea
  • inquiryId for Inquiry
  • userId for User

Issue 2: 주석 복붙 실수

문제:

// InquiryService
/**
 * 아이디어 삭제  // ❌ 복붙 실수!
 */
public void delete(Long inquiryId) {

해결:

/**
 * 문의사항 삭제  // ✅
 */
public void delete(Long inquiryId) {

교훈: 복붙 후 항상 주석 확인!


Issue 3: countByUserId 타입 혼용

문제:

public Long countByUserId(long userId) {  // Long vs long 혼용

해결:

public long countByUserId(Long userId) {  // ✅ 일관성

교훈: 타입 일관성 중요!

  • 파라미터: Long (객체형)
  • 반환: long (기본형, 개수는 null 없음)

📊 현재 상태

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

완료:
✅ 프로젝트 생성 (Day 1)
✅ H2 데이터베이스 설정 (Day 1)
✅ ERD 설계 (Day 2)
✅ Entity 3개 작성 (Day 2)
✅ JPA Auditing 설정 (Day 2)
✅ Repository 3개 구현 (Day 3) ← 오늘!
✅ Service 3개 구현 (Day 4) ← 오늘!

진행 중:
⏳ 없음

예정:
📋 JUnit 테스트 작성
📋 Controller 작성
📋 Postman API 테스트
📋 OAuth2 로그인 연동
📋 Python FastAPI 연동

🎯 다음 할 일 (Day 5)

필수

  1. JUnit 테스트 작성

    @SpringBootTest
    class IdeaServiceTest {
        @Test
        void 아이디어_저장_테스트() {
            // given
            Idea idea = new Idea(...);
    
            // when
            Idea saved = ideaService.save(idea);
    
            // then
            assertThat(saved.getIdeaId()).isNotNull();
        }
    }
  2. 간단한 Controller

    @RestController
    @RequestMapping("/api/ideas")
    public class IdeaController {
        @PostMapping
        public Idea create(@RequestBody Idea idea) {
            return ideaService.save(idea);
        }
    }

선택

  • Postman으로 API 테스트
  • CommandLineRunner로 샘플 데이터 생성

🔗 참고 자료


💬 회고

😊 잘한 점

  • 하루에 Repository + Service 모두 완성 (효율적!)
  • 각 메서드의 역할 명확히 이해
  • Optional 사용 전략 학습
  • PK vs FK 개념 정리
  • 실수를 통한 학습 (파라미터명, 주석)

😅 아쉬운 점

  • 개념 이해에 시간 소요 (Optional, 예외 처리)
  • 완벽 이해보다 실습 우선 필요
  • 너무 디테일에 집중

🎓 배운 점

  • Repository: Spring Data JPA가 자동 구현
  • Service: 비즈니스 로직 처리 계층
  • Optional vs 예외: 비즈니스 의미에 따라 선택
  • PK vs FK: 유일성 vs 중복 가능
  • @RequiredArgsConstructor: 생성자 자동 생성
  • Query Method: 메서드명으로 쿼리 자동 생성
  • Service 패턴: Repository + 비즈니스 로직

💡 깨달은 점

  • 완벽 이해보다 빠른 완성과 반복이 중요
  • 80% 이해하고 다음 프로젝트로!
  • 실무에서도 완벽한 코드는 없음
  • 작동하는 코드 → 리팩토링 순서
  • 디테일보다 큰 흐름 파악 우선

📈 성장 포인트

  • Repository 인터페이스만으로 CRUD 구현 이해
  • Service 계층의 역할과 패턴 습득
  • Optional 활용법 (언제 반환? 언제 예외?)
  • Spring Data JPA Query Method 작성법
  • Lombok을 통한 코드 간결화

🚀 Day 5에서 만나요!

테스트 코드와 Controller로 실제 동작하는 API를 만들어봅시다! 💪

+ Recent posts