🎯 프로젝트 목표
- Java 언어를 활용한 멀티스레드 프로그램 구현
- Spring Boot의 계층 구조 학습
- 클라이언트-서버 구조의 간단한 웹 서버 구현
🎮 게임 로직 구현 (파이썬)
더보기
import sys
N = 19
DIRS = ((0, 1), (1, 0), (1, 1), (-1, 1)) # 가로/세로/우하/우상
def count_consecutive(board, x, y, dx, dy, p):
cnt = 1
nx, ny = x + dx, y + dy
while 0 <= nx < N and 0 <= ny < N and board[nx][ny] == p:
cnt += 1
nx += dx
ny += dy
nx, ny = x - dx, y - dy
while 0 <= nx < N and 0 <= ny < N and board[nx][ny] == p:
cnt += 1
nx -= dx
ny -= dy
return cnt
def get_line(board, x, y, dx, dy, p, span=5):
# 0: 빈칸, 1: 내 돌, 2: 막힘(상대/경계)
line = []
for k in range(-span, span + 1):
nx, ny = x + k * dx, y + k * dy
if 0 <= nx < N and 0 <= ny < N:
v = board[nx][ny]
if v == p:
line.append(1)
elif v == 0:
line.append(0)
else:
line.append(2)
else:
line.append(2)
return line
def makes_open_four(line, center, newpos):
# 패턴: 0 1 1 1 1 0 (활사). 가운데/새 돌이 4개의 1 구간 안에 포함되어야 함
L = len(line)
for s in range(L - 5): # 길이 6 윈도우
if line[s] == 0 and line[s + 5] == 0 and line[s + 1:s + 5] == [1, 1, 1, 1]:
stones = range(s + 1, s + 5)
if center in stones and newpos in stones:
return True
return False
def has_four_in_direction(board, x, y, dx, dy, p):
# 길이 5 윈도우에 내 돌 4 + 빈칸 1 (한 수로 5목 가능) 이 있으면 '사'로 간주
line = get_line(board, x, y, dx, dy, p, span=5)
center = 5
L = len(line)
for s in range(L - 4): # 길이 5 윈도우
if s <= center <= s + 4:
w = line[s:s + 5]
if w.count(1) == 4 and w.count(0) == 1:
return True
return False
def has_open_three_in_direction(board, x, y, dx, dy, p):
# 빈칸 하나를 더 두면 활사(0 1111 0)가 되는 경우가 있으면 '활삼'으로 간주
line = get_line(board, x, y, dx, dy, p, span=5)
center = 5
for t, v in enumerate(line):
if v != 0:
continue
line[t] = 1 # 다음 수 가정
ok = makes_open_four(line, center, t)
line[t] = 0
if ok:
return True
return False
def is_forbidden_black_move(board, x, y):
# board[x][y] == 1(흑) 이 이미 놓여있다고 가정
# 1) 장목(6+) 금지
for dx, dy in DIRS:
if count_consecutive(board, x, y, dx, dy, 1) >= 6:
return True, "장목(6목 이상)"
# 2) 정확히 5목이면 즉시 승리(금수로 보지 않음)
for dx, dy in DIRS:
if count_consecutive(board, x, y, dx, dy, 1) == 5:
return False, None
# 3) 4-4 / 3-3 금지(흑만)
fours = 0
threes = 0
for dx, dy in DIRS:
if has_four_in_direction(board, x, y, dx, dy, 1):
fours += 1
else:
if has_open_three_in_direction(board, x, y, dx, dy, 1):
threes += 1
if fours >= 2:
return True, "4-4 금수"
if threes >= 2:
return True, "3-3 금수"
return False, None
def check_winner(board, x, y):
p = board[x][y]
for dx, dy in DIRS:
cnt = count_consecutive(board, x, y, dx, dy, p)
if p == 1:
if cnt == 5: # 흑: 정확히 5
return 1
else:
if cnt >= 5: # 백: 5 이상
return 2
return 0
def print_board(board):
sys.stdout.write("\n".join(" ".join(map(str, row)) for row in board) + "\n\n")
def main():
board = [[0] * N for _ in range(N)]
empties = N * N
player = 1
move_count = 0
while empties:
sys.stdout.write("입력: x y (0~18)\n")
sys.stdout.write(f"Player {player}'s Turn (1=흑, 2=백)\n")
sys.stdout.flush()
while True:
line = sys.stdin.readline()
if not line:
return
try:
x, y = map(int, line.split())
except ValueError:
sys.stdout.write("[Error] 입력은 'x y' 형태로 해주세요.\n")
continue
if not (0 <= x < N and 0 <= y < N):
sys.stdout.write("[Error] Invalid position\n")
continue
if board[x][y] != 0:
sys.stdout.write("[Error] Already located\n")
continue
board[x][y] = player
# 렌주룰: 흑(1)만 금수 체크
if player == 1:
forbidden, reason = is_forbidden_black_move(board, x, y)
if forbidden:
board[x][y] = 0
sys.stdout.write(f"[Renju] 금수입니다: {reason}\n")
continue
break
empties -= 1
move_count += 1
print_board(board)
if move_count >= 9:
winner = check_winner(board, x, y)
if winner:
sys.stdout.write(f"Player {winner} Win!\n")
return
player = 3 - player
sys.stdout.write("Draw!\n")
if __name__ == "__main__":
main()
- 백준 2072번 문제의 정답 코드를 기반으로 렌주룰(흑돌의 3-3, 4-4, 장목 금지)을 추가했다.
💻 프로젝트 생성
- 프로젝트 설정: 타입 - Gradle (Groovy) / JDK - corretto-17
- 프로젝트 의존성
- 웹 서버 역할을 해야 하므로 일단 Spring Web, Spring Web Service을 추가함
- Getter, Setter 사용을 위해 Lombok을 추가함
📁 프로젝트 계층 구조
ohmok/
build.gradle
settings.gradle
src/
main/
java/
com/example/ohmok/
OhmokApplication.java # 애플리케이션 시작점(부트스트랩)
controller/
GameController.java # API(입력/출력) 계층
dto/
MoveRequest.java # 요청 DTO
GameStateResponse.java # 응답 DTO
service/
GameService.java # 유스케이스/흐름 제어(서비스 계층)
RenjuRules.java # 렌주룰 판정(도메인 로직 성격)
model/
Game.java # 게임 상태(도메인 모델)
exception/
ApiException.java # 도메인/서비스에서 던지는 예외
GlobalExceptionHandler.java # 예외를 HTTP 응답으로 매핑
resources/
application.properties
test/
java/...
일반적으로 Spring Boot 애플리케이션의 계층 구조는 유지보수성과 확장성을 높이기 위해 다음과 같이 역할별로 구분된다.
- Controller 계층
▪ 클라이언트(웹, 모바일 등)로부터 들어오는 HTTP 요청을 처리
▪ 요청을 적절한 서비스에 전달하고, 서비스에서 처리된 결과를 응답으로 반환
▪@RestController,@RequestMapping등의 애노테이션 사용 - Service 계층
▪ 비즈니스 로직을 담당
▪ 여러 데이터 조작이나 검증, 트랜잭션 관리가 이뤄짐
▪ Controller와 데이터 접근 계층(Repository) 사이에서 중재자 역할 수행
▪ 주로@Service애노테이션이 붙음 - Repository (또는 DAO) 계층 - 이 프로젝트에서는 사용되지 않았지만 나중에 추가될 수도 있음
▪ 데이터베이스와 직접 연결되어 데이터를 저장, 조회, 수정, 삭제하는 역할
▪ JPA, MyBatis, JdbcTemplate 등을 사용
▪@Repository애노테이션으로 표시 - Domain (Model) 계층
▪ 데이터베이스 테이블과 매핑되는 엔티티 클래스들이 위치
▪ 객체지향적으로 데이터와 상태, 동작을 포함할 수도 있음
▪ DTO와 구분되며, 주로 DB와 연동되는 실제 데이터 구조
이 프로젝트에서 계층 간 의존 방향은 아래와 같이 위에서 아래로 이동한다.
Controller → Service → Domain(Model/Rules)
1. Presentation Layer (controller, dto)
- GameController
- HTTP 요청을 받고 (POST /games/{id}/moves)
- 입력을 최소 검증한 뒤
- GameService에 위임하고
- 결과를 DTO로 반환
- DTO(MoveRequest, GameStateResponse)
- API 입출력 전용 구조체
- 내부 도메인 모델(Game)을 그대로 노출하지 않게 해줌
2. Application/Service Layer (service)
- GameService
- 게임 생성, 한 수 두기, 게임 조회 같은 유스케이스를 담당함
- 턴 변경, 착수 가능 여부, 종료 처리 같은 흐름/상태 전이를 여기서 제어함
- 렌주 룰 체크(RenjuRules)와 승리 판정을 호출함
- 현재는 ConcurrentHashMap으로 게임을 메모리에 보관함
3. Domain Layer (model, rules)
- Game (model)
- 보드, 현재 플레이어, 승패, 수 카운트 같은 핵심 상태를 가짐
- RenjuRules
- “흑 금수(3-3/4-4/장목)”, “승리 조건(흑=정확히5, 백=5이상)” 같은 비즈니스 규칙을 담음
- 가능하면 Spring 의존성 없이 순수 로직으로 유지하는 게 좋음
4. Infrastructure
- DB, 캐시, 메시지큐 같은 인프라가 아직 없음
- 게임 저장소가 메모리(Map) 로 되어 있어 단일 서버에서만 유지됨
- 나중에 Redis/DB로 바꾸려면 GameService에 GameRepository 같은 인터페이스를 두고 코드를 교체하는 구조로 확장해야 함
코드 구현
first commit · qkrwns1478/oh-mok@f59c6b2
+ testImplementation 'org.springframework.boot:spring-boot-starter-webservices-test'
github.com
사용 예시
# 게임 생성
curl -X POST localhost:8080/api/games
# 수 두기 (x,y는 0~18)
curl -X POST localhost:8080/api/games/{gameId}/moves \
-H "Content-Type: application/json" \
-d '{"x":10,"y":10}'
📚 오늘의 TIL
1. Java의 Record 클래스
Java 14 이후 버전부터 도입된 record는 데이터를 담는 간단한 불변(immutable) 객체를 쉽게 정의하기 위한 새로운 클래스 유형이다. 주로 값 객체(Value Object)를 만들 때 사용한다.
- 불변 객체: 모든 필드는 기본적으로 final이며, 객체 생성 후 변경이 불가능함
- 간결한 문법: 필드 선언, 생성자, getter, equals(), hashCode(), toString() 메서드를 자동으로 생성해 코드가 매우 간결해짐
- 사용 용도
- 단순 데이터 전달 객체(DTO)
- 불변성을 보장해야 하는 값 객체
- 코드 간결성과 안전성을 동시에 추구할 때
- 일반 클래스와의 차이점
- 일반 클래스: 데이터뿐 아니라 다양한 행위(메서드)를 포함할 수 있고, 변경 가능한 상태를 가질 수 있다. 복잡한 객체를 설계할 때 주로 사용한다.
- record 클래스: 주로 불변(immutable) 값 객체로서 데이터만 담기 위해 간결하게 설계된 클래스다. 코드량을 크게 줄이고, 데이터 객체 설계에 최적화돼 있다.
- 일반 클래스 사용 예시
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// equals(), hashCode(), toString() 직접 작성 필요
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
- record 클래스 사용 예시
public record Person(String name, int age) {}
- 주요 차이점 정리
| 구분 | 일반 클래스 | 레코드 클래스 |
| 코드량 | 생성자, getter, equals, hashCode, toString 직접 작성 | 선언만으로 자동 생성됨 |
| 불변성 | 원하는 경우 final 필드를 직접 지정해야 함 | 기본적으로 모든 필드가 final이고 불변 |
| 가독성 및 간결함 | 코드가 길고 반복적 | 매우 간결하고 명확 |
| 확장성 | 메서드 추가와 상태 변경 가능 | 기본적으로 불변, 메서드 재정의 가능하지만 상태 변경은 불가 |
| 목적 | 복잡하거나 상태 변하는 객체 설계에 적합 | 단순 데이터 운반 및 불변 값 객체에 적합 |
2. Lombok
Lombok은 Java 개발에서 반복되는 보일러플레이트 코드를 줄여주는 라이브러리다. getter, setter, 생성자, equals, hashCode, toString 메서드를 자동으로 생성함으로써 코드가 훨씬 간결해지고, 개발 생산성이 향상된다.
- 컴파일 시점에 애노테이션을 처리하여 코드에 필요한 부분을 자동 생성함
- @Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor, @ToString, @EqualsAndHashCode, @Data 등 다양한 애노테이션 제공
- 코드 가독성 향상과 유지보수 효율성 증가에 도움
- 하지만 내부적으로는 비공개 컴파일러 API에 의존하는 등 일부 주의점이 있다.
▪ Lombok 사용 전 예시
package com.example.ohmok.model;
public class Game {
public static final int N = 19;
private final String id;
private final int[][] board = new int[N][N];
private int currentPlayer = 1; // 1=흑
private boolean finished = false;
private int winner = 0; // 0=없음/무승부
private int moveCount = 0;
private int empties = N * N;
public Game(String id) {
this.id = id;
}
public String getId() { return id; }
public int[][] getBoard() { return board; }
public int getCurrentPlayer() { return currentPlayer; }
public void setCurrentPlayer(int currentPlayer) { this.currentPlayer = currentPlayer; }
public boolean isFinished() { return finished; }
public void setFinished(boolean finished) { this.finished = finished; }
public int getWinner() { return winner; }
public void setWinner(int winner) { this.winner = winner; }
public int getMoveCount() { return moveCount; }
public void incMoveCount() { this.moveCount++; }
public int getEmpties() { return empties; }
public void decEmpties() { this.empties--; }
}
▪ Lombok 적용 후 예시
package com.example.ohmok.model;
import lombok.Getter;
import lombok.Setter;
@Getter
public class Game {
public static final int N = 19;
private final String id;
private final int[][] board = new int[N][N];
@Setter
private int currentPlayer = 1; // 1=흑
@Setter
private boolean finished = false;
@Setter
private int winner = 0; // 0=없음/무승부
private int moveCount = 0;
private int empties = N * N;
public Game(String id) {
this.id = id;
}
public void incMoveCount() { this.moveCount++; }
public void decEmpties() { this.empties--; }
}
📋 TO-DO 리스트
- Web GUI 구현
- 클라이언트-서버 로직 분리
'프레임워크 > Spring' 카테고리의 다른 글
| [Spring] JPA와 H2 DB (0) | 2026.03.10 |
|---|---|
| [Spring Boot] Spring CRUD 구현 - Day 1 (0) | 2026.03.10 |
| [Spring] Spring MVC (0) | 2026.03.09 |
| [Spring Boot] 오목 게임 만들기 - Day 2 (0) | 2026.02.23 |
| [Spring Boot] EC2 배포 가이드 (Backend & DB) (0) | 2025.11.25 |