본문 바로가기

우테코/오픈미션

오픈미션 14일차

이제 오늘은 오픈 미션이 마무리 되는 날이다. 제출 기한에 대해시작은 11월 17일 오후 3시부터이며 마감은 11월 25일 0시 라고 알려줬으며 제출 기한에 대해서 이것이 변함없는 사실이고 나머지 사항에 대해서는 각자 스스로 판단해 주시길 바란다고 했으니 나는 이번주동안 이번 프로젝트에 대해서 정리하고 통계 관련 기능을 추가하며 마무리해서 제출할 예정이다. 이제 정말 곧 끝이 난다는 게 실감 나는 날인 것 같다. 처음 시작할 때만 해도 프리코스가 엄청 길다고 생각했는데 지금 돌아보면 정말 어떻게 지나갔는 지도 모르게 빠르게 지나간 것 같다. 이 짧은 시간동안 우테코가 어떤 곳인지 경험할 수 있었던 것 같고 한 사람, 한 개발자로서 중요한 성장을 해본 것 같아서 뿌듯하다. 남은 시간 마무리를 잘해서 잘 마무리해보자!! 

 

 

우선 오늘은 리드미 작성을 해보았다. 이번 프리코스를 진행하면서 리드미에 대한 중요성을 알았고 누군가(코치분들 등)이 내 깃을 보고 웹을 테스트해보고 싶다고 했을 때 어떻게 해야하는 지 등? 을 작성해놔야 겠다는 생각이 들어 보다 꼼꼼하게 작성해보았다. 크게 카테고리는 5가지로 아래와 같이 구성해보았다. 

 

1. 프로젝트 설명

2. 프로젝트 기술 스택과 선정 이유

3. 프로젝트 실행 방법 

4. 프로젝트의 각 웹페이지 별 설명 

5. 구현한 기능 목록 

 

# 우아한 테크코스 오픈 미션 
 
### 1. 프로젝트 설명

본 프로젝트는 우아한테크코스 4주차 오픈미션입니다. 


3주차에 Java 콘솔 애플리케이션으로 구현했던 '로또 미션'을 'Spring Boot 웹 API'로 확장하고, 사용자가 실제 로또 번호를 기록하고 관리할 수 있는 웹 페이지를 구현하는 것을 목표로 합니다.


백엔드는 Spring Boot를 사용하여 RESTful API를 구축하고, 프론트엔드는 순수 HTML, CSS, JavaScript(Fetch API)를 사용하여 백엔드와 통신합니다.

 
---
 

### 2. 사용 기술 및 선정 이유

### Backend 

Java 21

Spring Boot: 웹 API 구축, 의존성 관리, 계층형 아키텍처(Controller, Service, Repository) 구현을 위해 사용했습니다.

Spring Data JPA: MyLotto, PurchasedLotto 등 도메인 엔티티의 데이터 영속성을 관리하기 위해 사용했습니다.

H2 Database: 개발 및 테스트 단계에서 별도 설치 없이 사용할 수 있는 인메모리 DB로, 빠른 실행과 검증을 위해 사용했습니다. 
(주의: 애플리케이션을 재실행할 때마다 데이터베이스는 초기화 됩니다.)

Lombok: @Getter, @RequiredArgsConstructor 등을 통해 반복적인 보일러플레이트 코드를 줄이기 위해 사용했습니다.


### Frontend

HTML / CSS (Pure): Spring Boot 백엔드 API 개발에 집중하기 위해, 프론트엔드는 가장 기본적이고 표준적인 기술을 사용했습니다.

JavaScript (Fetch API): 정적 HTML 페이지가 백엔드 API 서버와 '비동기'로 통신(데이터 요청/저장)하기 위해 사용했습니다.

### Test

JUnit 5 & Mockito: LottoService 비즈니스 로직이 의존성(Repository)과 분리되어 정확히 동작하는지 검증하는 단위 테스트를 위해 사용했습니다.

Postman / Web Browser: 구현된 API 엔드포인트(LottoController)가 LottoApplication 실행 시 실제 HTTP 요청에 대해 의도대로 작동하는지 통합 테스트하기 위해 사용했습니다.

 
---
 

### 3. 실행 및 테스트 방법

1. 본 프로젝트를 로컬 환경에 클론(Clone)합니다.

2. IDE(IntelliJ 등) 또는 터미널을 사용하여 Gradle 프로젝트를 빌드합니다.

3. com.woowa.lotto.LottoApplication의 main 메서드를 실행하여 Spring Boot 서버를 시작합니다.

4. 서버가 정상적으로 켜지면 8080 포트가 활성화됩니다.

5. 웹 브라우저를 열고 http://localhost:8080/ 주소로 접속하여 웹 페이지를 확인할 수 있습니다.

(참고: Spring Boot가 src/main/resources/static 폴더의 index.html을 자동으로 제공합니다.)

(선택) Postman과 같은 API 테스트 도구를 사용하여 http://localhost:8080/im-minji/ 경로의 API 엔드포인트를 직접 테스트할 수 있습니다.

 
---
 

### 4. 페이지별 기능 설명

모든 페이지는 src/main/resources/static 경로에 HTML 파일로 존재하며, JavaScript fetch를 통해 백엔드 API와 통신합니다.

#### 1. index.html (홈 화면)

- 프로젝트의 개요와 목적을 설명하는 메인 페이지입니다.

#### 2. random-lotto.html (랜덤 로또 생성)

- 랜덤 번호를 생성할 수 있습니다. (재생성도 가능)
- 생성한 랜덤 번호를 이름과 함께 MyLotto(나만의 로또)에 저장할 수 있습니다. 


#### 3. my-lotto.html (나만의 로또 목록)

- 저장된 나만의 로또 목록(테이블)을 조회할 수 있습니다. 
- 목록의 구매 버튼을 통해 각 로또를 구매할 수 있습니다. (구매 시 purchasedLotto로 날짜와 함께 로또 복사본이 이동됩니다.)
- 삭제 버튼을 통해 각 로또를 삭제할 수 있습니다. 

#### 4. my-lotto-add.html (나만의 로또 입력)
- 로또 번호와 이름을 수동으로 입력해 myLotto 목록에 추가할 수 있습니다.


#### 5. purchased-lotto.html (구매 로또 목록)
- 저장된 구매 로또 목록(테이블)을 조회할 수 있습니다.
- 삭제 버튼을 통해 각 로또를 삭제할 수 있습니다.


#### 6. purchased-lotto-add.html (구매 로또 입력)
- 로또 번호와 구매 날짜를 수동으로 입력해 purchasedLotto 목록에 추가할 수 있습니다.

 
---
 

### 5. 구현한 기능 목록 (API 기준)

#### 1) 로또 (Lotto)

- GET /im-minji/lotto/random: 6개 랜덤 로또 번호 생성 (DB 저장 X, 오름차순 정렬)

 

#### 2) 나만의 로또 (MyLotto)

- POST /im-minji/my-lotto: myLotto 1개 저장 (랜덤/수동)



- GET /im-minji/my-lotto: myLotto 전체 목록 조회 (ID 오름차순 정렬)



- DELETE /im-minji/my-lotto/{id}: myLotto 1개 삭제

 

#### 3) 구매 로또 (PurchasedLotto)

- POST /im-minji/purchase/manual: purchasedLotto 1개 수동 저장 (날짜 선택 가능)



- POST /im-minji/purchase/from-my-lotto/{id}: myLotto를 purchasedLotto로 복사 저장 (오늘 날짜로 저장)



- GET /im-minji/purchase: purchasedLotto 전체 목록 조회 (구매 날짜 최신순 정렬)



- DELETE /im-minji/purchase/{id}: purchasedLotto 1개 삭제

 

그래서 오늘은 3시 혹은 그 전에 올라올 오픈 미션 제출 방식을 보고? 다시 한 번 언제 제출할 지 정한 다음 앞으로 할 것을 정하려고 했으나 시스템 변경 문제로 내일 3시로 늦춰져서 오늘 하루는 뭔가 붕떠버렸다. 하지만 이 시간을 놓치고 싶지는 않아 당첨 통계에 대해서 어떻게 구현할 지 지금까지 했던 것을 토대로 설계를 해보았다. 

 

 

내가 이번에 진행한 프로젝트의 설계 순서는 객체 구현 (+ 단위 테스트) -> 그에 맞는 서비스 구현 (+ 단위테스트) -> 서비스를 이용하는 컨트롤러 구현 (+ 포스트맨 테스트) -> 프론트 구현 -> fetch -> 통합 테스트 (웹에서 직접 테스트)  이와 같았다. 

 

 

그래서 당첨 통계 기능이 무엇이고 그것을 하루이틀만에 어떻게 구현할 것인지 고민해보았다. 

 

 

우선 당첨 통계 기능은 3주차 미션의 요구사항으로 구현했던 LottoRank와 LottoResult라는 것을 활용하려는 목적이다.

 

 

내가 생각한 이 기능의 요구사항을 정리해보면

1. 구매 로또에 날짜를 부여하였는 데 그 날짜 별로 구매 로또를 나누는 것이다. (예를 들어 2025년 11월 1주차, 2주차 등으로 구분)
2. 나눌때는 깔끔하게 월~일로 설정 (원래는 일~토 고 토요일에 당첨번호가 나오기는 한데 그럼 복잡해질 것 같아 나눈다는 거에 의미를 두었다.)
3. 새로 추가할 당첨 통계 페이지에서 주차별 당첨번호를 입력할 수 있다. 
4. 그렇게 되면 그 주의 로또 번호와 당첨번호를 비교해서 1~5등 중 몇개가 당첨되었는 지에 대한 것들을 보여준다.
5. 또한 구매한 금액(로또의 목록 개수)과 당첨 금액에 대한 수익률도 보여준다. 

 

 

3주차에 구현했던 객체

1. LottoRank

package lotto;

import java.util.Arrays;

public enum LottoRank {
    FIRST(6, 2000000000L, false),
    SECOND(5, 30000000L, true),
    THIRD(5, 1500000L, false),
    FOURTH(4, 50000L, false),
    FIFTH(3, 5000L, false),
    NONE(0, 0, false);

    private final int winningCount;
    private final long winningPrize;
    private final boolean isNeedBonus;

    LottoRank(int winningCount, long winningPrize, boolean needBonus) {
        this.winningCount = winningCount;
        this.winningPrize = winningPrize;
        this.isNeedBonus = needBonus;
    }

    public boolean matches(int count, boolean bonus) {
        // 'FIRST', 'FOURTH', 'FIFTH' 는 보너스 여부에 관심이 없다.
        if (this.winningCount != 5) {
            return this.winningCount == count; // 보너스 있고 없고를 전달할 필요가 없음
        }

        // 'SECOND', 'THIRD' (5개인 경우)
        return this.winningCount == count && this.isNeedBonus == bonus;
    }

    public static LottoRank find(int winningCount, boolean hasBonus) {
        return Arrays.stream(LottoRank.values()) // 모든 Rank 상수를 가져와서
                .filter(rank -> rank.matches(winningCount, hasBonus)) // 각자 matches?
                .findAny() // "true"라고 대답한 랭크를 찾아서 반환
                .orElse(LottoRank.NONE); // 아무도 없으면 NONE을 반환
    }

    public int getWinningCount() {return winningCount;}
    public long getWinningPrize() {return winningPrize;}
    public boolean isNeedBonus() {return isNeedBonus;}
}

 

2. LottoResult

package lotto;

import java.util.Collections;

import java.util.Map;
import java.util.Objects;

public class LottoResult {
    private static final int MIN_PURCHASE_PRICE = 0;
    private static final double DEFAULT_RATE_OF_RETURN = 0.0;
    private static final double PERCENTAGE_MULTIPLIER = 100.0;

    private final Map<LottoRank, Integer> statistics;

    public LottoResult(Map<LottoRank, Integer> statistics) {
        this.statistics = Objects.requireNonNull(statistics, "[ERROR] 통계판이 비워져 있습니다.");
    }

    // 2. 총상금 계산 (private long calculateTotalWinningPrize())
    private double calculateTotalWinningPrize() {
        double totalWinningPrize = 0;

        for (Map.Entry<LottoRank, Integer> entry : this.statistics.entrySet()) {
            long prize = entry.getKey().getWinningPrize();
            int count = entry.getValue();
            totalWinningPrize += (double) prize * (double) count;
        }
        return totalWinningPrize;
    }

    public double getRateOfReturn(int purchaseAmount) {
        double totalWinningPrize = calculateTotalWinningPrize();

        if (purchaseAmount == MIN_PURCHASE_PRICE) {
            return DEFAULT_RATE_OF_RETURN;
        }
        return (totalWinningPrize / (double)purchaseAmount) * PERCENTAGE_MULTIPLIER;
    }

    public Map<LottoRank, Integer> getStatistics() {
        return Collections.unmodifiableMap(this.statistics);
    }
}

 

이 두 개의 객체는 @Entity로 활용되지는 않는다. 그 이유는 객체의 역할이 데이터 베이스에 저장될 데이터가 아니라 비즈니스 로직을 수행하는 규칙이기 때문이다.

1. @Entity의 역할 (예: MyLotto, PurchasedLotto, WinningLotto)

  • 정의: 데이터베이스에 저장되고, 조회되고, 수정되고, 삭제되어야 하는 데이터
  • 특징:
    • @Id (예: MyLotto 1번, MyLotto 2번...)
    • JPA가 객체의 생명주기(Lifecycle)를 직접 관리한다. 
    • 데이터베이스의 테이블과 1:1로 매핑된다.
    • 데이터를 저장하고 불러오기 위해 Repository (예: MyLottoRepository)가 필요하다. 

2. LottoRank / LottoResult의 역할 (순수 Java 객체/Enum)

  • 정의: 비즈니스 규칙을 정의하거나, 특정 로직을 수행하기 위해 임시로 생성되어 사용되는 계산용 객체이다. 

 

1) LottoRank (Enum):

  • 이것은 "6개 일치하면 1등", "5개+보너스 일치하면 2등"이라는 규칙 그 자체이다. 
  • 이 규칙은 DB에 저장될 데이터가 아니라, Java 코드 자체에 상수(Constant)로 정의되어 LottoService가 가져다 쓰는 도구이다. 

2 )LottoResult: 

  • LottoService가 통계를 계산할 때, 등수별 맵(Map<LottoRank, Integer>)과 수익률을 계산하는 동안만 잠시 들고 있기 위한 객체이다. 
  • 통계 계산이 끝나고 StatisticsResponseDTO가 만들어지면, 이 LottoResult 객체는 메모리에서 사라진다. 
  • 특징:
    • @Id가 없다.
    • JPA가 관리하지 않는다. 
    • Repository가 필요 없다. 

 

그래서 당첨번호는 WinningLotto에 저장되고 LottoRank와 LottoResult는 활용되는 규칙들이다. 

 

그 다음으로는 당첨 통계 기능을 위해서 당첨 번호를 보내는 requestDTO 1개와 결과를 내기 위해서 데이터를 받아오는 responseDTO 1개를 구현했다. 

package com.woowa.lotto.dto.request;

import java.time.LocalDate;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class WinningLottoCreateRequestDTO {
    // 당첨 번호 6개
    private List<Integer> numbers;

    // 보너스 번호 1개
    private Integer bonusNum;

    // 당첨 번호 날짜 
    private LocalDate drawDate;
}

 

package com.woowa.lotto.dto.response;

import com.woowa.lotto.domain.LottoRank;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;

@Getter
@Builder
public class StatisticsResponseDTO {

    // 추첨일
    private final LocalDate drawDate;

    // 해당 주차의 당첨 번호
    private final List<Integer> winningNumbers;

    // 보너스 번호
    private final Integer bonusNum;

    // 등수별 당첨 횟수
    private final Map<LottoRank, Integer> rankCounts;

    // 수익률
    private final double rateOfReturn;
}

 


3. 서비스와 컨트롤러 설계

LottoController

 

 

LottoService


이제 할 일은 DTO를 활용해서 서비스를 만들고 서비스에게 일을 시키는 컨트롤러(API)를 만들어야 한다. 구현은 설계도를 바탕으로 해야징 프리코스를 하면서 요구사항을 미리 생각하고 작성해두는 것에 대한 중요성을 깨달았기 때문에 자연스럽게 오픈 미션을 하면서도 내가 스스로 요구사항과 각 파일의 설계도를 작성한 후 구현하려고 노력중이다! 위 내용을 다 구현한 후 프론트를 살짝 수정해서 API와 연결해야 한다. 내일 최대한 프론트까지 수정해 API를 연결해보고 이번 프로젝트는 마무리할 것이다. 그리고 주말동안 리드미와 이번 프로젝트를 정리하고 소감문을 작성해서 제출할 것이다! 뭔가 끝난다는 게 아쉽지만 후회없이 했으니 끝까지 열심히 해서 잘 마무리 할 것이다 아자자

 

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

오픈미션 16일차  (0) 2025.11.19
오픈미션 15일차  (0) 2025.11.18
오픈미션 13일차  (0) 2025.11.16
오픈미션 12일차  (0) 2025.11.15
오픈미션 11일차  (0) 2025.11.14