본문 바로가기

우테코/오픈미션

오픈미션 7일차

아니 벌써 일주일이 지났다니 시간이 너무 빠른 것 같다. 남은 일주일동안 더 열심히 도전하고 몰입해서 멋진 결과를 만들어내고 성장하고 싶다💪🏼

 

오늘은 눈뜨자마자 JPA를 위해서 객체를 어떻게 설계할지에 대해서 UML 형태로 정리해보았다. 

내가 만든 객체는 총 6개인데 그 중 LottoRank와 LottoResult는 계산과 규칙을 위한 객체여서 해당이 안되고 나머지 4개의 객체에 대해서 설계했다! 

- JPA 설계 패턴 

  1. 클래스: @Entity
  2. private Long id; : @Id, @GeneratedValue
  3. protected 기본 생성자(JPA용)를 추가
  4. '핵심 객체'(Lotto 객체)은 @OneToOne 어노테이션으로 관계를 맺는다.
  5. '단순 객체'(String, int, LocalDate)은 그냥 필드로 둔다.
  6. '검증 로직'(validate...)은 public 생성자 또는 private 메서드에 둔다. (JPA가 신경 쓰지X)

 

 

그리고 나서 JpaRepository 설계를 시작했다. repository 패키지 안에 객체 각각에 대해서 4개의 repository를 만들어야 한다. 이 4개의 인터페이스는 JpaRepository를 상속(extends)받으면 다음 메서드들은 구현하지 않아도 사용 가능하다. 

  • save(Entity): (저장/수정)
  • findById(ID): (조회)
  • findAll(): (조회)
  • delete(Entity): (삭제)

 

이렇게 설계를 마치고 이제 구현에 들어가야 한다!!! 

 

1. Lotto

package com.woowa.lotto.domain;

import java.util.List;
import jakarta.persistence.*;

@Entity
public class Lotto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 로또 한장을 저장하는 객체
    @ElementCollection
    @CollectionTable(name = "lotto_numbers", joinColumns = @JoinColumn(name = "lotto_id"))
    private final List<Integer> numbers;

    protected Lotto() {
        this.numbers = null;
    }

    public Lotto(List<Integer> numbers) {
        validate(numbers);
        this.numbers = numbers;
    }



    // 이 객체는 로또 한 장에 해당하는 데이터를 이용해 검증, 보너스 숫자 존재, 당첨번호와 몇 개 일치하는 지 스스로 확인
    private void validate(List<Integer> numbers) {
        // 로또 번호 개수 확인
        if (numbers.size() != 6) {
            throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
        }

        // 로또 숫자 범위 1~45 확인
        for(Integer num : numbers) {
            if(num < 1 || num > 45) {
                throw new IllegalArgumentException("[ERROR] 로또 번호의 범위는 1~45여야 합니다.");
            }
        }

        // 로또 숫자가 서로 중복되지 않는 지 확인
        if(numbers.size() != numbers.stream().distinct().count()) {
            throw new IllegalArgumentException("[ERROR] 로또 번호는 서로 중복되면 안됩니다.");
        }

    }

    // TODO: 추가 기능 구현
    // 보너스 번호가 리스트 중에 존재하는지 확인해서 true/false 를 돌려주는 메서드
    public boolean hasBonusNum(int bonusNum) {
        return numbers.contains(bonusNum);
    }

    // 당첨 번호와 몇 개가 일치하는 확인해서 일치하는 개수를 돌려주는 메서드
    public int matchCount(Lotto otherLotto) {
        List<Integer> otherNumbers = otherLotto.getNumbers();

        long count = this.numbers.stream()
                .filter(otherNumbers::contains)
                .count();
        return (int) count;
    }

    public List<Integer> getNumbers() {return numbers;}

    public Long getId() {return id;}
}

 

 

2. MyLotto

package com.woowa.lotto.domain;

import java.util.Objects;
import jakarta.persistence.*;

@Entity
public class MyLotto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(cascade = CascadeType.ALL) // MyLotto가 저장/삭제될 때, Lotto도 같이 저장/삭제
    @JoinColumn(name = "lotto_id")
    private final Lotto myLotto;

    @Column
    private final String lottoName;

    protected MyLotto() {
        this.myLotto = null;
        this.lottoName = null;
    }

    public MyLotto(Lotto myLotto, String lottoName) {
        this.myLotto = Objects.requireNonNull(myLotto, "[ERROR] 로또가 비워져 있습니다.");

        Objects.requireNonNull(lottoName, "[ERROR] 로또 이름은 null일 수 없습니다.");

        validateLottoName(lottoName);
        this.lottoName = lottoName;
    }

    private void validateLottoName(String lottoName) {
        if(lottoName.length() > 20 || lottoName.isEmpty()) {
            throw new IllegalArgumentException("[ERROR] 로또 이름은 1~20글자 범위로 입력해주세요.");
        }
    }

    public Lotto getMyLotto() {return myLotto;}

    public String getLottoName() {return lottoName;}

    public Long getId() {return id;}
}

 

 

3. PurchasedLotto

package com.woowa.lotto.domain;

import java.time.LocalDate;
import java.util.Objects;
import java.time.temporal.WeekFields;
import java.util.Locale;
import jakarta.persistence.*;

@Entity
public class PurchasedLotto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "lotto_id")
    private final Lotto purchasedLotto;

    @Column
    private final LocalDate purchaseDate;

    protected  PurchasedLotto() {
        this.purchasedLotto = null;
        this.purchaseDate = null;
    }

    public PurchasedLotto(Lotto purchasedLotto, LocalDate purchaseDate) {
        this.purchasedLotto = Objects.requireNonNull(purchasedLotto, "[ERROR] 로또가 비워져 있습니다.");
        this.purchaseDate = Objects.requireNonNull(purchaseDate, "[ERROR] 날짜가 비워져 있습니다.");
    }

    public boolean isPurchasedInWeek(LocalDate dateToCompare) {
        WeekFields weekRule = WeekFields.of(Locale.KOREA);

        // 2. 나의 '연도'와 '주차 번호'를 가져옵니다.
        int myYear = this.purchaseDate.getYear();
        int myWeekNumber = this.purchaseDate.get(weekRule.weekOfWeekBasedYear());

        // 3. 비교할 날짜의 '연도'와 '주차 번호'를 가져옵니다.
        int compareYear = dateToCompare.getYear();
        int compareWeekNumber = dateToCompare.get(weekRule.weekOfWeekBasedYear());

        // 4. 두 개의 '연도'와 '주차 번호'가 "둘 다" 같은지 확인합니다.
        return (myYear == compareYear) && (myWeekNumber == compareWeekNumber);
    }

    public Lotto getPurchasedLotto() {return purchasedLotto;}
    public LocalDate getPurchaseDate() {return purchaseDate;}
    public Long getId() {return id;}
}

 

 

4. WinningLotto

package com.woowa.lotto.domain;

import java.time.LocalDate;
import jakarta.persistence.*;
import java.util.Objects;

@Entity
public class WinningLotto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 당첨 번호를 가지는 객체 (6개 번호 + 1개의 보너스 번호)
    @OneToOne(cascade = CascadeType.ALL)
    private final Lotto winningLotto;

    @Column
    private final Integer bonusNum;

    @Column
    private final LocalDate drawDate;

    protected WinningLotto() {
        this.winningLotto = null;
        this.bonusNum = null;
        this.drawDate = null;
    }

    public WinningLotto(Lotto winningLotto, Integer bonusNum, LocalDate drawDate) {
        validate(winningLotto, bonusNum);
        this.winningLotto = winningLotto;
        this.bonusNum = bonusNum;
        this.drawDate = drawDate;
    }

    // 보너스 번호를 가지는 객체이므로 보너스 번호에 대한 검증은 winningLotto가 해야함
    private void validate(Lotto winningLotto, Integer bonusNum) {
        Objects.requireNonNull(bonusNum, "[ERROR] 보너스 번호는 null일 수 없습니다.");

        if(bonusNum < 1 || bonusNum > 45) {
            throw new IllegalArgumentException("[ERROR] 보너스 번호의 범위는 1~45여야 합니다.");
        }

        if(winningLotto.hasBonusNum(bonusNum)) {
            throw new IllegalArgumentException("[ERROR] 보너스 번호는 당첨 번호와 중복되면 안됩니다.");
        }
    }

    // 서비스에서 보너스 번호, 당첨번호, 만든 날짜를 사용하기 위한 getter
    public Lotto getWinningLotto() {return winningLotto;}

    public Integer getBonusNum() {return bonusNum;}

    public LocalDate getDrawDate() {return drawDate;}

    public Long getId() {return id;}

}

 

Lotto만 구현하고 나니 나머지 객체들은 어렵지 않게 구현할 수 있었다. 다만 WinningLotto에서 bonusNum이 int로 선언되어 있어 null로 생성자를 구현할 수 없어 보너스 숫자에 관한 것은 Integer로 객체로 수정했다. 그리고 Lotto 자체를 활용하는 경우에는 cascade를 사용해서 연관성을 이어줬다. 항상 sql 같은 표 형식 데이터베이스정도만 설계해보다가 이렇게 코드로 데이터베이스를 구현한다는게 어색하고 신기했다. 이 다음으로는 각 객체에 연결되는 repository 인터페이스를 구현했다. JpaRepository를 상속만 받으면 기본 기능을 다 사용할 수 있다니 와우~

 

1. LottoRepository

package com.woowa.lotto.repository;

import com.woowa.lotto.domain.Lotto;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface LottoRepository extends JpaRepository<Lotto, Long> {
}

 

2. MyLottoRepository

package com.woowa.lotto.repository;

import com.woowa.lotto.domain.MyLotto;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MyLottoRepository extends JpaRepository<MyLotto, Long> {
}

 

3. PurchasedRepository

package com.woowa.lotto.repository;

import com.woowa.lotto.domain.PurchasedLotto;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PurchasedLottoRepository extends JpaRepository<PurchasedLotto, Long> {
    List<PurchasedLotto> findAllByPurchaseDateBetween(LocalDate start, LocalDate end);
}

 

4. WinningLottoRepository

package com.woowa.lotto.repository;

import com.woowa.lotto.domain.WinningLotto;
import java.time.LocalDate;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface WinningLottoRepository extends JpaRepository<WinningLotto, Long> {
    Optional<WinningLotto> findByDrawDate(LocalDate drawDate);
}

 

우선 뭔가 해보기는 했는데 정말 이걸로 레포지토리가 작동하는지는? 모르겄따... 인터페이스 상속만 받고 설계할 때 커스텀 메서드가 필요한 경우에는 추가를 해줬다. 근데 이 메서드도 안에는 다 비워져있고 Jpa가 알아서 해준다고 한다..? 흠 

 

지금까지 한 것은

1. Jpa가 도메인을 인식할 수 있도록 객체에 @Entity, @Column, @ID 등의 어노테이션 추가하기

2. Jpa Repository 기능을 사용할 수 있도록 인터페이스 상속받아 구현하기

 

이제 할 것은 1,2번으로 정말 둘이 연결이 되었는지? 단위테스트로 테스트해보기! 

domain과 repository가 H2 데이터베이스와 연동되는지 확인하기 위해, MyLottoRepository에 대한 단위 테스트(@DataJpaTest)를 실행했다.

package com.woowa.lotto.repository;

import com.woowa.lotto.domain.Lotto;
import com.woowa.lotto.domain.MyLotto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

// H2 같은 인메모리 DB를 자동으로 실행하고, 끝나면 자동 롤백
@DataJpaTest
class MyLottoRepositoryTest {

    // 테스트에 필요한 repository 2개 추가: MyLotto를 저장하려면 '부품'인 Lotto도 저장해야 하므로 2개 다 필요
    @Autowired
    private MyLottoRepository myLottoRepository;

    @Autowired
    private LottoRepository lottoRepository;

    @DisplayName("MyLotto 객체를 저장하고, ID로 다시 찾을 수 있다.")
    @Test
    void saveAndFindById() {
        Lotto lottoEngine = new Lotto(List.of(1, 2, 3, 4, 5, 6));
        Lotto savedLottoEngine = lottoRepository.save(lottoEngine);

        MyLotto myLotto = new MyLotto(savedLottoEngine, "나의 행운 번호");

        MyLotto savedMyLotto = myLottoRepository.save(myLotto);

        Optional<MyLotto> foundMyLottoOptional = myLottoRepository.findById(savedMyLotto.getId());

        assertThat(foundMyLottoOptional).isPresent();

        MyLotto foundMyLotto = foundMyLottoOptional.get();
        assertThat(foundMyLotto.getLottoName()).isEqualTo("나의 행운 번호");
        
        assertThat(foundMyLotto.getMyLotto().getId()).isEqualTo(savedLottoEngine.getId());
    }
}

 

 

결과

> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
12:40:47.557 [Test worker] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.woowa.lotto.repository.MyLottoRepositoryTest]: MyLottoRepositoryTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
12:40:47.604 [Test worker] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.woowa.lotto.LottoApplication for test class com.woowa.lotto.repository.MyLottoRepositoryTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.7)

2025-11-10T12:40:47.739+09:00  INFO 88624 --- [lotto] [    Test worker] c.w.l.repository.MyLottoRepositoryTest   : Starting MyLottoRepositoryTest using Java 21.0.8 with PID 88624 (started by imminji in /Users/imminji/Desktop/git/lotto)
2025-11-10T12:40:47.740+09:00  INFO 88624 --- [lotto] [    Test worker] c.w.l.repository.MyLottoRepositoryTest   : No active profile set, falling back to 1 default profile: "default"
2025-11-10T12:40:47.861+09:00  INFO 88624 --- [lotto] [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2025-11-10T12:40:47.881+09:00  INFO 88624 --- [lotto] [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 17 ms. Found 4 JPA repository interfaces.
2025-11-10T12:40:47.903+09:00  INFO 88624 --- [lotto] [    Test worker] beddedDataSourceBeanFactoryPostProcessor : Replacing 'dataSource' DataSource bean with embedded version
2025-11-10T12:40:47.933+09:00  INFO 88624 --- [lotto] [    Test worker] o.s.j.d.e.EmbeddedDatabaseFactory        : Starting embedded database: url='jdbc:h2:mem:9711f2b8-cb70-4d92-ae94-abee6608eb86;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
2025-11-10T12:40:48.030+09:00  INFO 88624 --- [lotto] [    Test worker] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2025-11-10T12:40:48.045+09:00  INFO 88624 --- [lotto] [    Test worker] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.6.33.Final
2025-11-10T12:40:48.055+09:00  INFO 88624 --- [lotto] [    Test worker] o.h.c.internal.RegionFactoryInitiator    : HHH000026: Second-level cache disabled
2025-11-10T12:40:48.149+09:00  INFO 88624 --- [lotto] [    Test worker] o.s.o.j.p.SpringPersistenceUnitInfo      : No LoadTimeWeaver setup: ignoring JPA class transformer
2025-11-10T12:40:48.175+09:00  INFO 88624 --- [lotto] [    Test worker] org.hibernate.orm.connections.pooling    : HHH10001005: Database info:
	Database JDBC URL [Connecting through datasource 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory$EmbeddedDataSourceProxy@d28c214']
	Database driver: undefined/unknown
	Database version: 2.3.232
	Autocommit mode: undefined/unknown
	Isolation level: undefined/unknown
	Minimum pool size: undefined/unknown
	Maximum pool size: undefined/unknown
2025-11-10T12:40:48.487+09:00  INFO 88624 --- [lotto] [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate: drop table if exists lotto cascade 
Hibernate: drop table if exists lotto_numbers cascade 
Hibernate: drop table if exists my_lotto cascade 
Hibernate: drop table if exists purchased_lotto cascade 
Hibernate: drop table if exists winning_lotto cascade 
Hibernate: create table lotto (id bigint generated by default as identity, primary key (id))
Hibernate: create table lotto_numbers (numbers integer, lotto_id bigint not null)
Hibernate: create table my_lotto (id bigint generated by default as identity, lotto_id bigint unique, lotto_name varchar(255), primary key (id))
Hibernate: create table purchased_lotto (purchase_date date, id bigint generated by default as identity, lotto_id bigint unique, primary key (id))
Hibernate: create table winning_lotto (bonus_num integer, draw_date date, id bigint generated by default as identity, winning_lotto_id bigint unique, primary key (id))
Hibernate: alter table if exists lotto_numbers add constraint FKjygr57v329fiddscag72upl74 foreign key (lotto_id) references lotto
Hibernate: alter table if exists my_lotto add constraint FKbvrahjnautgsykjnrr3d22k6o foreign key (lotto_id) references lotto
Hibernate: alter table if exists purchased_lotto add constraint FKp656mcein2dt79ytubhngsy9b foreign key (lotto_id) references lotto
Hibernate: alter table if exists winning_lotto add constraint FKtrsgkv2afixd3i90g2qwimu2a foreign key (winning_lotto_id) references lotto
2025-11-10T12:40:48.509+09:00  INFO 88624 --- [lotto] [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2025-11-10T12:40:48.650+09:00  INFO 88624 --- [lotto] [    Test worker] c.w.l.repository.MyLottoRepositoryTest   : Started MyLottoRepositoryTest in 1.02 seconds (process running for 1.44)
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
WARNING: A Java agent has been loaded dynamically (/Users/imminji/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy-agent/1.17.8/f09415827a71be7ed621c7bd02550678f28bc81c/byte-buddy-agent-1.17.8.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
Hibernate: insert into lotto (id) values (default)
Hibernate: insert into my_lotto (lotto_name,lotto_id,id) values (?,?,default)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-11-10T12:40:48.977+09:00  INFO 88624 --- [lotto] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
Hibernate: drop table if exists lotto cascade 
Hibernate: drop table if exists lotto_numbers cascade 
Hibernate: drop table if exists my_lotto cascade 
Hibernate: drop table if exists purchased_lotto cascade 
Hibernate: drop table if exists winning_lotto cascade 
> Task :test
BUILD SUCCESSFUL in 6s
4 actionable tasks: 2 executed, 2 up-to-date
12:40:49 PM: Execution finished ':test --tests "com.woowa.lotto.repository.MyLottoRepositoryTest"'.

 

BUILD SUCCESSFUL로 테스트가 통과했으며, 실행 로그(Log)를 통해 다음을 확인할 수 있었다. 

1. H2 인메모리 DB 자동 실행

  • 로그: Starting embedded database: url='jdbc:h2:mem:...'
  • @DataJpaTest 어노테이션이 작동하여, 별도 설정 없이도 H2 의존성을 감지하고 테스트용 '인메모리 DB'를 자동으로 실행했다.

2. @Entity 스캔 및 테이블 자동 생성 

  • 로그: Hibernate: create table lotto ..., create table my_lotto ..., create table purchased_lotto ...
  • JPA(Hibernate)가 domain 패키지에 있는 @Entity 어노테이션들을 인'했다.
  • 인식의 증거로, Lotto, MyLotto 등 4개 @Entity에 매핑되는 DB 테이블 5개(Lotto의 @ElementCollection 포함)를 H2 DB에 자동으로 생성했다. (domain 1단계 설계가 성공했음을 증명)

3. JpaRepository 자동 구현 및 작동 

  • 로그: Found 4 JPA repository interfaces.
  • repository 패키지에 선언한 4개의 JpaRepository 인터페이스를 스프링이 발견하고 자동 구현체를 주입(@Autowired)했다.

4. save / find (CRUD) 연동 성공 

  • 로그: Hibernate: insert into lotto ..., insert into my_lotto ...
  • 테스트 코드의 myLottoRepository.save() 메서드가 JPA를 통해, H2 DB가 알아듣는 INSERT SQL 쿼리로 정상 번역및 실행되었다.
  • findById()로 다시 '조회'하고 assertThat()으로 '검증'하는 테스트가 최종 BUILD SUCCESSFUL로 통과했다.

 

이렇게 해서 도메인과 레포지토리 구현은 완료했다! (아닐지도 모르지만...) 이제 오늘은 서비스 설계를 한 후 마무리할 것이다. 졸업을 위한 토익시험을 일요일로 접수해버려서 그것도 얼른 공부해야한다 그래서 아침에 일어나자마자 오늘 구현 계획을 1차 마무리한 후 이제 다른 것들을 하다가 저녁에 다시 서비스 설계를 해야할 것 같다 아자자!! 

 

 

 

 

마지막으로 오늘이 딱 7일 남은 시점이므로 오늘부터 제출일까지의 계획을 설정해보았다

D-7 (월, 11/10): domain 수정 및 repository TDD (1단계: JPA 연동)

  • [핵심 목표] domain이 H2 DB에 저장되는 지 확인하기 
  • [To-Do]
    1. domain 수정: @Entity 객체 4개(Lotto, MyLotto 등)에 @Id, @GeneratedValue, @OneToOne, protected 생성자 등 어노테이션을 붙여 '엔티티'로 변환.
    2. repository 구현: lotto.repository 패키지에 4개의 JpaRepository 인터페이스 생성.
    3. TDD (핵심): src/test에 repository 패키지를 만들고, @DataJpaTest 어노테이션을 사용한 MyLottoRepositoryTest.java 작성
    4. 검증: myLottoRepository.save(myLotto)가 성공하는지, myLottoRepository.findById(id)가 저장된 객체를 잘 반환하는지 테스트

 

D-6 (화, 11/11): service 설계 및 TDD (2단계: 비즈니스 로직 Part 1)

  • [핵심 목표] Service가 Repository를 호출하는 로직 구현 
  • [To-Do]
    1. service 설계: LottoService 클래스와 구현할 메서드(예: generateRandomLotto, saveToMyLotto, purchaseFromMyLotto)의 껍데기를 설계
    2. service 구현 (단순 로직): '랜덤 번호 생성', '나만의 로또 저장' 등 '계산'이 적은 기능부터 구현.
    3. TDD: @SpringBootTest (또는 Mockito)를 사용한 LottoServiceTest.java 작성. Repository를 '가짜(Mock)'로 만들거나, '실제 DB(@DataJpaTest처럼)'를 사용해 Service의 '단순 로직'이 잘 작동하는지 검증.

 

D-5 (수, 11/12): service 구현 (2단계: 비즈니스 로직 Part 2)

  • [핵심 목표] 통계 계산 로직 구현
  • [To-Do]
    1. service 구현 (복잡 로직): getWeeklyStatistics(LocalDate drawDate) 메서드 구현 (이 메서드는 WinningLottoRepository와 PurchasedLottoRepository를 Join하고, LottoRank와 LottoResult를 활용해야함)
    2. TDD: Service 테스트 코드에 '통계 검증' 로직 추가
    3. 검증: '가짜' 구매 내역과 '가짜' 당첨 번호를 DB에 save한 뒤, getWeeklyStatistics를 호출했을 때, LottoResult가 정확한 '수익률'(예: 62.5)을 반환하는지 assertThat으로 증명하기

D-4 (목, 11/13): controller 설계 및 구현 (3단계: API)

  • [핵심 목표] 웹에서 Service로 요청이 잘 연결되는지 확인하기
  • [To-Do]
    1. dto 설계: controller가 service와 주고받을 데이터 봉투(LottoPurchaseRequest 등) 설계
    2. controller 구현: @RestController, @PostMapping, @GetMapping, @RequestBody를 사용해 API  구현.
    3. 테스트 (핵심): Postman (또는 IntelliJ HTTP Client)을 사용해, localhost:8080/api/lotto/purchase 같은 API 직접 호출해보기
    4. 검증: API 호출 시, H2 DB에 데이터가 저장되는지 확인. (H2 콘솔 접속)

 

D-3 (금, 11/14): 프론트 연동 및 통합 테스트 (4단계: End-to-End)

  • [핵심 목표] 기능만 볼 수 있도록 Controller API를 호출하는 최소한의 UI 구현
  • [To-Do]
    1. resources/static 폴더에 index.html, app.js 파일 생성.
    2. app.js에서 fetch() API를 사용해 Controller의 API(예: GET /api/lotto/random)를 호출.
    3. API가 반환한 JSON 데이터를 console.log로 찍거나, div에 간단히 표시.
    4. (버퍼) 이 과정에서 100% 발생하는 'CORS' 에러, JSON 파싱 에러 등 '통합 버그' 수정.

 

오늘하는 것부터는 정말 처음 해보는 것들이여서 걱정이 많았는데 계속 도전하고 성공하고 하는 느낌이여서 기분이 좋았던 것 같다. 발전해나가는 느낌은 항상 좋다. 물론 제일 큰 산인 서비스와 컨트롤러가 남아있지만 나는 해낼 것이다!!! 계획대로 열심히 해보자구☀️ 아자자

그리고 토,일요일 계획은 목요일쯤에 다시 한 번 계획을 세워볼 것이다. 17일 3시부터 제출이라고 하였는데 마감은 또 25일이여서 어떻게 되는 건지 잘 모르겄다. 우선은 월요일 제출을 목표로 달려가보자!! 

'우테코 > 오픈미션' 카테고리의 다른 글

오픈미션 9일차  (0) 2025.11.12
오픈미션 8일차  (0) 2025.11.11
오픈미션 6일차  (0) 2025.11.09
오픈미션 5일차  (0) 2025.11.08
오픈미션 4일차  (1) 2025.11.07