1. Dockerfile 작성

내가 구현한 모듈을 Docker 이미지로 만들기 위한 설명서인 Dockerfile을 작성한다.

Python과 FastAPI 기반이므로, uvicorn이라는 비동기 서버로 실행하도록 구성하였다.

# 베이스 이미지: 파이썬 3.11 슬림 버전
FROM python:3.11-slim

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 소스 코드 복사
COPY . .

# 서버 실행 (main.py 파일의 app 객체를 8000 포트로 실행)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

 

CMD 부분의 "main:app" 부분을 본인 프로젝트에 맞춰서 작성하면 된다.

[파이썬 파일명]:[FastAPI 앱 변수명] 형식으로 지정

 

2. Docker 이미지 빌드 및 Docker Hub에 푸시

먼저 로컬 PC의 프로젝트 폴더에서 터미널을 열고 아래 명령어를 입력한다.

마지막 '.'이 현재 디렉토리의 Dockerfile을 사용하라는 의미이므로 꼭 포함시켜줘야 한다.

docker build -t {DockerHubID}/{프로젝트명} .

 

빌드된 이미지는 현재 로컬 PC에만 존재하므로 중앙 이미지 저장소인 Docker Hub에 올려준다.

docker login
docker push {DockerHubID}/{프로젝트명}

 

+ Mac OS라면 아래 트러블 슈팅으로 넘어가주세여

 

 

3. AWS EC2 준비 및 배포

AWS에서 Ubuntu를 사용하는 EC2 인스턴스를 하나 생성하고,

보안 그룹 설정을 통해 SSH(22번)와 HTTP(80번) 포트 등등을 열어주었다.

로컬 터미널에서 pem 키 통해서 이 서버에 접속한다.

ssh -i "키페어이름.pem" ubuntu@<EC2-퍼블릭-DNS-주소>

 

Docker를 설치하고

Docker Hub에 올려두었던 프로젝트 이미지를 가져온다.

docker pull {DockerHubID}/{프로젝트명}

 

마지막으로, 내려받은 이미지를 컨테이너로 실행하면 끝 !!!

# -d: 컨테이너를 백그라운드에서 계속 실행 (detached mode)
# -p 80:8000: EC2 서버의 80번 포트로 들어온 요청을 컨테이너의 8000번 포트로 전달

docker run -d -p 80:8000 --name my-ai-app {DockerHubID}/{프로젝트명}

 


🚨트러블 슈팅

 

1. 의존성 충돌

처음에 우분투의 패키지 매니저인 apt를 이용하여 도커를 설치하였더니 의존성 충돌 에러가 발생했다.

# 시도했던 명령어
sudo apt-get install docker.io

 

에러 메시지: The following packages have unmet dependencies: containerd.io : Conflicts: containerd

우분투의 containerd와 도커의 containerd.io가 같은 시스템 자원을 두고 충돌을 일으킨 것이었다.

 

해결 방법 :  기존 패키지 완전 삭제 -> 공식 스크립트로 설치

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

 

2. no matching manifest 아키텍처 불일치

 

에러 메시지: no matching manifest for linux/amd64 in the manifest list entries

 

원인은 CPU 아키텍처의 불일치였다.

개발 환경인 내 로컬 컴퓨터는 M3 Mac이므로 arm64 아키텍처 기반이지만,

실행 환경인 AWS EC2 인스턴스는 amd64 아키텍처 기반의 CPU를 사용한다.

 

해결 방법: Docker가 지원하는 buildx 라는 CLI 플러그이니 사용

buildx로 타겟 플랫폼 지정

docker buildx build 명령어에 --platform 플래그를 추가하여, 이미지가 실행될 환경인 linux/amd64를 명시적으로 지정해준다.

# M3 맥(로컬) 터미널에서 실행
# --push 플래그 사용하여 빌드 끝나자마자 푸시하도록
docker buildx build --platform linux/amd64 -t {DockerHubID}/{프로젝트명} --push .

 

1️⃣ @DynamicInsert와 @DynamicUpdate가 무엇인지 ?

JPA의 구현체인 하이버네이트(Hibernate)에서 제공하는 어노테이션이다.

엔티티의 INSERT 및 UPDATE 쿼리를 생성하는 방식을 최적화하기 위해 사용된다.

하이버네이트가 INSERT 또는 UPDATE 쿼리를 고정된 형태가 아닌,

실제 엔티티의 상태에 따라 동적으로 생성하도록 하여 성능상 이점을 얻기 위해 사용하는 어노테이션 !

 

2️⃣ @DynamicInsert와 @DynamicUpdate가 어떻게 작동되는 지 ?

기존 방식

  1. JPA (정확히는 하이버네이트와 같은 JPA 구현체)는 기본적으로 엔티티의 영속성 상태 변화를 감지하여 자동으로 SQL 쿼리를 생성하고 실행한다.이 방식은 쿼리 생성 로직이 단순하다는 장점이 있지만, 불필요하게 많은 컬럼을 포함시키는 단점이 있다.
  2. 예시 
    1. INSERT: 기본적으로 모든 컬럼을 INSERT 문에 포함시킴
    2. UPDATE: 업데이트 가능한 모든 컬럼을 UPDATE 문의 SET 절에 포함시킴.

 

@DynamicInsert, @DynamicUpdate 적용

  1. 1. 엔티티 클래스 레벨에 @DynamicInsert 를 붙이면 적용된다.
  2. 예시
    1. INSERT: null이 아닌 필드들만 INSERT 문에 포함시킴
    2. UPDATE: 엔티티가 컨텍스트에 로딩된 후 실제로 값이 변경된 필드들만 SET절에 포함시킴

 

 

3️⃣ 적용 시 장단점

구분 기존 @DynamicInsert/Update
장점 쿼리 생성 로직이 단순하여 하이버네이트 내부 처리 부하가 적음 - 쿼리 문자열이 짧아지기 때문에 네트워크 부하가 줄어듦
- 불필요한 컬럼의 삽입/갱신을 막아 데이터베이스 부하를 줄일 수 있음
- null 값 삽입시 DB의 DEFAULT값이 적용되도록 유도할 수 있음.
- 실제 변경된 컬럼만 갱신하여 동시성 충돌 가능성을 줄일 수 있음
단점 - 불필요하게 많은 컬럼을 쿼리에 포함시켜 쿼리 문자열이 길어지고 네트워크 부하가 증가할 수 있음
- 데이터베이스의 DEFAULT 값이 아닌 명시적인 NULL이 삽입될 수 있음 (@DynamicInsert 시 기대하는 동작과 다를 수 있음)
- 값이 변경되지 않은 컬럼도 다시 갱신하여 불필요한 쓰기 작업이 발생할 수 있음.
어떤 컬럼을 쿼리에 포함시킬지 결정하는 추가적인 내부처리가 필요하여 하이버네이트 내부 처리 부하가 소폭 증가할 수 있음

 

 

4️⃣ 언제 적용하면 좋을까 ?

  • 엔티티의 컬럼 수가 매우 많은 경우
    • 컬럼 수가 많을 수록 불필요하게 포함되는 컬럼이 많아져 쿼리 문자열이 길어지고 네트워크 및 데이터베이스 부하가 커진다.
  • INSERT 시 대부분의 컬럼이 null이거나 DB의 DEFAULT 값을 사용하고 싶은 경우
    • @DynamicInsert → 명시적으로 null을 삽입하는 대신 DB의 기본값을 사용하도록 유도
  • UPDATE 시 변경되는 컬럼의 수가 적은 경우
    • 전체 컬럼 중 실제로 몇 개만 변경되는 경우가 빈번하다면, 변경된 컬럼만 갱신함으로써 데이터베이스 쓰기 부하를 줄이고 쿼리 크기를 최적화할 수 있음.
  • 성능 최적화가 필요한 와이드(Wide) 테이블 (컬럼이 많은 테이블): 대규모 데이터 처리에서 쿼리 크기나 불필요한 쓰기 작업이 병목이 될 수 있다면 고려해볼 수 있음.

🚨 주의사항

  • @DynamicInsert와 @DynamicUpdate는 하이버네이트의 확장 기능이며 표준 JPA 기능은 아니다.
  • 성능 측정 없이는 명확한 효과를 단정하기 어려움. 때로는 기본 동작이 더 나은 성능을 보이기도 함.
  • 단순히 컬럼 수가 적고 변경이 잦은 테이블에는 큰 이점이 없을 수 있으며, 오히려 내부 처리 부하 때문에 미미하게나마 성능 저하가 있을 수 있음.

문제

인체에 치명적인 바이러스를 연구하던 연구소에서 바이러스가 유출되었다. 다행히 바이러스는 아직 퍼지지 않았고, 바이러스의 확산을 막기 위해서 연구소에 벽을 세우려고 한다.

연구소는 크기가 N×M인 직사각형으로 나타낼 수 있으며, 직사각형은 1×1 크기의 정사각형으로 나누어져 있다. 연구소는 빈 칸, 벽으로 이루어져 있으며, 벽은 칸 하나를 가득 차지한다. 

일부 칸은 바이러스가 존재하며, 이 바이러스는 상하좌우로 인접한 빈 칸으로 모두 퍼져나갈 수 있다. 새로 세울 수 있는 벽의 개수는 3개이며, 꼭 3개를 세워야 한다.

예를 들어, 아래와 같이 연구소가 생긴 경우를 살펴보자.

2 0 0 0 1 1 0
0 0 1 0 1 2 0
0 1 1 0 1 0 0
0 1 0 0 0 0 0
0 0 0 0 0 1 1
0 1 0 0 0 0 0
0 1 0 0 0 0 0

 

이때, 0은 빈 칸, 1은 벽, 2는 바이러스가 있는 곳이다. 아무런 벽을 세우지 않는다면, 바이러스는 모든 빈 칸으로 퍼져나갈 수 있다.

2행 1열, 1행 2열, 4행 6열에 벽을 세운다면 지도의 모양은 아래와 같아지게 된다.

2 1 0 0 1 1 0
1 0 1 0 1 2 0
0 1 1 0 1 0 0
0 1 0 0 0 1 0
0 0 0 0 0 1 1
0 1 0 0 0 0 0
0 1 0 0 0 0 0

바이러스가 퍼진 뒤의 모습은 아래와 같아진다.

2 1 0 0 1 1 2
1 0 1 0 1 2 2
0 1 1 0 1 2 2
0 1 0 0 0 1 2
0 0 0 0 0 1 1
0 1 0 0 0 0 0
0 1 0 0 0 0 0

벽을 3개 세운 뒤, 바이러스가 퍼질 수 없는 곳을 안전 영역이라고 한다. 위의 지도에서 안전 영역의 크기는 27이다.

연구소의 지도가 주어졌을 때 얻을 수 있는 안전 영역 크기의 최댓값을 구하는 프로그램을 작성하시오.

입력

첫째 줄에 지도의 세로 크기 N과 가로 크기 M이 주어진다. (3 ≤ N, M ≤ 8)

둘째 줄부터 N개의 줄에 지도의 모양이 주어진다. 0은 빈 칸, 1은 벽, 2는 바이러스가 있는 위치이다. 2의 개수는 2보다 크거나 같고, 10보다 작거나 같은 자연수이다.

빈 칸의 개수는 3개 이상이다.

출력

첫째 줄에 얻을 수 있는 안전 영역의 최대 크기를 출력한다.

 

 


코드

import sys
import copy
from collections import deque
input = sys.stdin.readline

n, m = map(int, input().split())
matrix = [list(map(int, input().split())) for _ in range(n)]

def bfs_game():
    game_matrix = copy.deepcopy(matrix)
    dx = [1, -1, 0, 0]
    dy = [0, 0, 1, -1]
    q = deque()
    for i in range(n):
        for j in range(m):
            if game_matrix[i][j] == 2:
                q.append((j, i))
   
    while q:
        x, y = q.popleft()
        for i in range(4):
            nx = x + dx[i]
            ny = y + dy[i]
            if 0 <= nx < m and 0 <= ny < n and game_matrix[ny][nx] == 0:
                game_matrix[ny][nx] = 2
                q.append((nx, ny))
    
    global maxCnt
    cnt = 0

    for i in range(n):
        cnt += game_matrix[i].count(0)
    
    maxCnt = max(maxCnt, cnt)

def make_wall(cnt):
    if cnt == 3:
        bfs_game()
        return 
    
    for i in range(n):
        for j in range(m):
            if matrix[i][j] == 0:
                matrix[i][j] = 1
                make_wall(cnt+1)
                matrix[i][j] = 0


maxCnt = 0
make_wall(0)
print(maxCnt)

 

bfs로 바이러스가 퍼지는 함수를 구현하고, 안전 영역 최댓값 구하는 건 쉬웠는데,

처음부터 어렵게 생각해서 벽을 세우는 알고리즘을 고민하는 데 시간을 많이 썼다.

 

무조건 3개의 벽을 세워야 했기 때문에 모든 경우의 수를 찾아보는 수 밖에 없었다.

하지만, 이때 효율적으로 탐색하기 위해 백트랙킹 방법을 사용하였다.

 

#벽을 세우는 함수 (재귀 + 백트래킹)
def make_wall(cnt):
    # 3-1. 베이스 케이스: 벽 3개를 모두 세웠다면
    if cnt == 3:
        bfs_game() # 바이러스 확산 시뮬레이션 실행
        return # 재귀 호출 종료

    # 3-2. 재귀 단계: 벽을 세울 위치 탐색
    # 연구소의 모든 칸을 순회
    for i in range(n):
        for j in range(m):
            # 현재 칸이 빈 칸(0)이라면 벽을 세울 수 있음
            if matrix[i][j] == 0:
                # 벽 세우기 시도
                matrix[i][j] = 1 # 현재 칸에 벽(1)을 임시로 세움
                # 다음 벽을 세우기 위해 재귀 호출 (cnt + 1)
                make_wall(cnt + 1)
                # 백트래킹: 재귀 호출이 끝난 후, 세웠던 벽을 다시 빈 칸(0)으로 되돌림
                # 이렇게 해야 다음 반복문에서 다른 위치에 벽을 세우는 경우를 탐색할 수 있음
                matrix[i][j] = 0

 

make_wall(cnt + 1) 호출이 끝나고 돌아오면, matrix[i][j] = 0을 통해 방금 세웠던 임시 벽을 다시 제거한다.

백트래킹을 해야만, 현재 위치 (i, j)에 벽을 세우는 경우를 모두 탐색한 후, 이 벽을 제거하고 다음 빈 칸에 벽을 세우는 경우를 시도할 수 있다. 이 과정을 통해 모든 가능한 "빈 칸 3개 선택" 조합을 탐색하게 된다.

 

DriveMate 에서 음성 챗봇 대화를 끝낸 뒤 client에서 main server로 chat log를 보내주면,

chatlog도 저장하고, 대화의 한줄 요약과 대화 키워드들도 추출해서 저장한다.

추출된 요약문과 키워드는 다음 대화에서 더욱 더 개인화된 대화를 구현하기 위해 사용된다.

 

1. 프로젝트 설정

먼저, Open API를 이용하기 위해서는 OpenAI에 신용카드를 등록해야 한다.

이후 발급된 API Key를 헤더에 넣어서 request를 보내면 된다.

1.1 의존성 추가

SpringBoot 프로젝트에서 ChatGPT와 통신하기 위해 RestTemplate 또는 WebClient를 사용할 수 있다.

나는 RestTemplate를 사용하여 구현하였다. 

build.gradle 파일에 spring-boot-starter-web 의존성을 추가하였다.

implementation 'org.springframework.boot:spring-boot-starter-web'

 

 

1.2 OpenAI API Key 설정

application.properties에 openAI 정보를 설정한다.

openai.api.key={your-api-key-here}
openai.api.url=https://api.openai.com/v1/chat/completions

 

 

2. ChatGptService 클래스 구현

chatGPT API와의 통신을 처리하는 ChatGptService 클래스를 작성한다.

이 클래스는 대화 내용을 받아서 요약과 키워드를 추출하고, 이를 반환하는 역할을 한다.

본인이 GPT를 이용해서 어떤 명령을 처리하고, 어떤 출력을 받고 싶은 지 구현하면 된다.

@Service
public class ChatGptService {

    @Value("${openai.api.key}")
    private String apiKey;

    @Value("${openai.api.url}")
    private String apiUrl;

    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    public ChatGptService(RestTemplate restTemplate, ObjectMapper objectMapper) {
        this.restTemplate = restTemplate;
        this.objectMapper = objectMapper;
    }

    public GptResponseDto generateSummaryAndKeywords(String conversation) {
        String prompt =
                "Please summarize the following conversation in one sentence and extract key keywords. Provide the summary and keywords **in Korean**. The summary must be only one sentence.\n" +
                "\n" +
                "Summary: {your summary}\n" +
                "Keywords: {keyword1, keyword2, keyword3}\n" +
                "\n" +
                "Conversation:\n" +
                chatLog;

        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + apiKey);
        headers.setContentType(MediaType.APPLICATION_JSON);
        
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("model", "gpt-4");
        requestBody.put("max_tokens", 150);

        List<Map<String, String>> messages = new ArrayList<>();
        messages.add(createMessage("system", "You are a helpful assistant that summarizes conversations and extracts keywords."));
        messages.add(createMessage("user", prompt));

        requestBody.put("messages", messages);

        String body = "{"
                + "\"model\": \"text-davinci-003\","
                + "\"prompt\": \"" + prompt + "\","
                + "\"max_tokens\": 150"
                + "}";

        String jsonBody;
        try {
            jsonBody = objectMapper.writeValueAsString(requestBody);
        } catch (Exception e) {
            throw new GeneralException(ErrorStatus.AI_BODY_ERROR);
        }

        HttpEntity<String> request = new HttpEntity<>(jsonBody, headers);

        ResponseEntity<String> response = restTemplate.exchange(apiUrl, HttpMethod.POST, request, String.class);

        try {
            JsonNode responseJson = objectMapper.readTree(response.getBody());
            JsonNode firstChoice = responseJson.get("choices").get(0);
            JsonNode messageNode = firstChoice.get("message");

            if (messageNode == null) {
                throw new GeneralException(ErrorStatus.EXTERNAL_API_ERROR);
            }

            JsonNode contentNode = messageNode.get("content");
            if (contentNode == null) {
                throw new GeneralException(ErrorStatus.EXTERNAL_API_ERROR);
            }

            String getResponse = contentNode.asText().trim();

            String[] parts = getResponse.split("Keywords:", 2);
            String summary = parts[0].trim();
            String keywords = parts.length > 1 ? parts[1].trim() : "";

            if (summary.startsWith("Summary:")) {
                summary = summary.substring("Summary:".length()).trim();
            }

            return GptResponseDto.GptSummaryKeywordDto.builder()
                    .summary(summary)
                    .keywords(keywords)
                    .build();

        } catch (Exception e) {
            throw new GeneralException(ErrorStatus.CHATGPT_PARSING_ERROR);
        }

    }
}

 

위 코드에서 generateSummaryAndKeywords 메소드는 대화 내용을 ChatGPT에 전달하고, 응답에서 요약과 키워드를 추출하여 반환한다. 이때, 요청의 형식은 Summary Keywords {}로 감싸서 받도록 프롬프트를 설정했다.

 

2.2 Controller

대화 로그를 받아 ChatGPT API와 연결하고, 요약과 키워드를 반환하는 API를 작성하였다.

@PostMapping("/process")
    public ResponseEntity<GptResponseDto> processConversation(
    		@RequestBody String conversation,
            @AuthenticationPrincipal CustomUserPrincipal userPrincipal) {
            
        // GPT 서비스 호출하여 요약 및 키워드 추출
        GptResponseDto gptResponse = chatGptService.generateSummaryAndKeywords(conversation, userPrincipal);

        return ApiResponse.onSuccess(SuccessStatus._OK, response);
    }

 

포스트에는 GPT 연결과 실질적인 사용을 위한 코드를 올려두었고,

그 외에 DB에 저장하는 로직이나, 다른 처리들은 생략하였다.

 

+ 결제한 모델에 따라서 코드나 설정이 달라지므로, 주의하기 !

 


결과

 

'SpringBoot' 카테고리의 다른 글

@DynamicInsert, @DynamicUpdate  (0) 2025.05.19
서블릿 vs. Spring MVC 비교  (0) 2025.04.07
AOP(Aspect-Oriented Programming)  (0) 2025.04.07
[SpringBoot] 자동 삭제 기능 구현 Scheduled Job  (0) 2025.01.21
[SpringBoot] API & Paging  (0) 2024.11.22

TDD 란 ?

Test Driven Development의 약자로 '테스트 주도 개발'을 뜻한다.

단순히 테스트 코드를 작성하는 기법이 아니라, 소프트웨어 개발의 한 방법론이자 개발 철학에 가깝다.

 

TDD는 실패하는 테스트 코드를 먼저 작성한 후, 그 테스트 코드를 통과시키기 위한 최소한의 실제 코드를 작성하고,

마지막으로 코드를 개선(리팩토링)하는 과정을 반복하는 개발 방식이다.

이름대로 테스트가 개발 과정을 이끌어 나가는 방식이라고 볼 수 있다.

 

핵심 과정 : ( Red-Green-Refactor Cycle)

 

TDD는 다음의 세 단계를 매우 짧은 주기로 반복하며 진행된다.

 

1. 레드(RED)

  • 실패하는 테스트 코드를 먼저 작성한다.
  • 새로 구현하려는 기능의 요구사항을 정의하고, 이 요구사항을 만족하는 코드가 아직 없기 때문에 당연히 이 테스트 코드는 실행 시 실패해야 한다.
  • 어떤 기능을 만들고 싶은 지, 그 기능이 어떤 입력에 대해 어떤 출력을 내보내야 하는 지를 테스트 코드를 먼저 명확하게 정의하는 단계이다.
  • 테스트가 왜 실패하는 지 확인하는 것까지가 중요하다.

 

2. 그린(GREEN)

  • 실패하는 테스트 코드를 통과시키기 위한 '최소한의' 실제 코드를 작성한다.
  • 이 단계에서는 코드의 품질이나 설계의 아름다움보다는 오직 테스트를 통과시키는 것에 집중한다.
  • 코드가 중복되거나 비효율적이어도 상관 없다.
  • 테스트가 통과하여 상태가 'Green'으로 바뀌는 것을 확인하는 것이 목표이다.

 

3. 리팩터(REFACTOR)

  • 테스트 코드가 모두 통과하는 상태(Green 상태)를 유지하면서 코드의 품질을 개선한다.
  • 중복된 코드를 제거하거나, 함수/클래스를 더 작고 명확하게 분리하거나, 변수 이름을 더 잘 이해할 수 있도록 바꾸는 등 코드의 구조와 가독성을 개선한다.
  • 리팩터링 후에도 모든 테스트가 여전히 통과되는 지 다시 한번 확인한다.
  • 테스트가 안전망 역할을 해주기 때문에 코드 변경 시 안정성을 확보할 수 있다.

위 사이클을 새로운 기능을 추가하거나 기존 기능을 수정할 때마다 반복한다.

각 사이클은 짧게는 몇 분에서 길게는 몇십 분 내로 완료하는 것을 목표로 한다.

 

TDD의 장점

TDD는 처음에는 개발 속도를 늦추는 것처럼 느껴질 수 있지만, 장기적으로 볼 때 다음과 같은 많은 이점을 제공한다.

 

1. Better Design

  • 코드를 작성하기 전에 코드를 사용하는 방법(테스트 코드)를 먼저 고민하게 만든다.
  • 이는 실제 사용할 때 편리하고 모듈성이 높은 코드를 설계하도록 유도한다.
  • 테스트하기 쉬운 코드는 일반적으로 의존성이 낮으며 응집도가 높다.

2. Reduced Bugs

  • 요구사항을 테스트 코드로 먼저 명확하게 정의하면서 설계 단계에서 많은 잠재적인 버그나 예외 상황을 미리 발견하고 고려하게 된다.
  • 잘 작성된 테스트 스위트(Test Suite)는 코드 변경 시 예상치 못한 Regression을 방지하는 강력한 안전망 역할을 한다.

3. Increased Confidence

  • 모든 기능이 자동화된 테스트를 통해 검증되므로, 코드가 예상대로 정확하게 동작한다는 확신을 가질 수 있다.
  • 이는 코드 변경이나 기능 추가 시 두려움을 줄여준다.

4. Living Documentation

  • 테스트 코드는 해당 기능이 어떻게 사용되어야 하는 지를 보여주는 가장 정확하고 항상 최신 상태로 유지되는 역할을 한다.
  • 새로운 개발자가 코드베이스를 이해하는 데 큰 도움이 된다.

5. Faster Development in the Long Run

  • 초기 테스트 작성 시간 때문에 속도가 느려 보이는 것 같지만, 디버깅 시간이 현저히 줄어들고 버그 수정 및 회귀 테스트 비용이 감소하면서 장기적으로는 전체 개발 속도가 향상된다.

+ TDD가 디버깅을 없애는 것은 아니다. 하지만 디버깅에 소요되는 시간과 노력을 크게 줄여준다.

 

일반 개발 방식과 TDD 의 차이

일반적인 개발 방식 : 요구사항 분석 -> 설계 -> 개발 -> 테스트 -> 배포

  • 코드를 먼저 작성하고, 기능이 잘 작동하는지 테스트를 통해 확인한다.
  • 주로 기능 구현 후, 버그 수정 및 테스트를 진행하는 형태이다.
  • 테스트는 기능 개발 후에 추가되거나, 필요한 경우에만 작성된다.

.테스트의 역할:

  • 일반 개발 방식:
    • 테스트는 개발 후에 기능이 정상적으로 동작하는지 확인하는 수단으로 사용된다.
    • 개발 완료 후에도 기능이 변경될 때마다 추가적인 테스트나 수정이 필요하다.
  • TDD:
    • 테스트는 개발의 출발점
    • 테스트는 코드가 개발되기 전부터 정의되며, 코드가 테스트를 통과할 때까지 반복적으로 작성되고 수정된다.
    • 테스트가 항상 코드를 검증하는 중요한 도구로 기능한다.

 

TDD의 대표적인 Tool

JUnit란 ?

Java 언어로 작성된 애플리케이션에서 단위 테스트를 작성하고 실행할 수 있도록 도와주는 테스트 프레임워크이다.

JUnit은 TDD(Test-Driven Development)를 실천하는 데 중요한 역할을 하며, 코드가 요구 사항을 충족하는지 자동으로 검증하는 데 사용된다.

JUnit을 사용하면 수동으로 테스트를 실행할 필요 없이 테스트를 자동으로 실행할 수 있다.

이를 통해 테스트 자동화가 가능해지고, 코드 변경 시 테스트를 반복적으로 실행하여 버그를 빨리 찾을 수 있다.

또한, JUnit은 테스트가 서로 독립적으로 실행되도록 보장한다.

 

'CS' 카테고리의 다른 글

Microsoft Azure에서 제공하는 API 디자인 모범 사례  (0) 2025.04.04
[소프트웨어 공학] UML  (1) 2024.10.20

전통적인 서블릿(Servlet) 기반 개발

  • HttpServlet : 서블릿은 HttpServlet 클래스를 상속받아 사용한다. doGet()과 doPost() 메서드를 오버라이드하여 HTTP GET 및 POST 요청을 처리하며, 요청을 받으면 직접 처리하고 응답을 작성하는 형태로 동작한다.

doGet() / doPost() 방식:

  • doGet(): 클라이언트가 GET 방식으로 서버에 요청할 때 호출되는 메서드. 주로 데이터를 조회하는 데 사용.
  • doPost(): 클라이언트가 POST 방식으로 서버에 요청할 때 호출되는 메서드. 데이터 전송 및 처리에 사용.

서블릿에서는 클라이언트의 요청을 하나하나 처리하며, 직접적으로 요청과 응답을 관리해야 하므로 코드가 길고, 관리가 어려움

또한 여러 요청을 처리할 때마다 조건문 등을 사용하여 각 요청에 맞는 처리 로직을 작성해야 하므로 코드가 복잡해질 수 있음.

 

 

Spring MVC

  • @Controller: Spring MVC에서 클라이언트 요청을 처리하는 클래스는 @Controller로 선언한다. 이는 서블릿에서 직접 처리하던 요청을 좀 더 구조적이고 간편하게 처리할 수 있도록 해준다.
  • @RequestMapping: @RequestMapping은 요청 URL과 메서드를 매핑하여 처리하는 데 사용된다. 이를 통해 각 요청에 대한 처리 로직을 명확히 분리할 수 있다.
  • DispatcherServlet: Spring MVC의 핵심 컴포넌트로, 모든 요청을 중앙에서 처리한다. 클라이언트 요청이 들어오면, DispatcherServlet이 해당 요청을 어떤 컨트롤러가 처리할지 결정하고, 실제 처리할 컨트롤러에게 요청을 위임한다.

Spring MVC는 클라이언트 요청을 분리된 처리 방식으로 구조화하여 코드의 유지보수성과 확장성을 높인다.

서블릿 기반 개발은 요청과 응답을 직접 처리하는 반면, Spring MVC는 컨트롤러, 서비스, 뷰 등의 역할을 명확하게 분리하여 개발자가 각 부분을 독립적으로 관리할 수 있도록 도와준다.

 

 

Spring MVC가 왜 서블릿보다 편리한지?

  • Spring MVC는 웹 애플리케이션 개발을 위한 표준화된 구조를 제공한다. 이를 통해 개발자는 직접 서블릿을 작성하고, 요청을 처리하는 복잡한 작업을 하지 않아도 된다.
  • 구조화된 개발 : Spring MVC는 MVC(Model-View-Controller) 패턴을 따르므로, 비즈니스 로직UI 로직컨트롤러를 분리하여 코드가 더 깔끔하고 이해하기 쉬운 구조로 나누어 진다.
  • 핸들러 매핑 : Spring MVC는 요청 URL을 특정 컨트롤러 메서드에 매핑하여, 개발자가 직접 URL을 처리할 필요 없이 쉽게 로직을 처리할 수 있다.
  • 자동화된 요청 처리: DispatcherServlet을 통해 모든 요청을 중앙에서 처리하고, 이를 적절한 컨트롤러와 매핑하는 방식으로 개발자는 복잡한 요청 처리 로직을 직접 작성할 필요가 없다.
  • 유연성: Spring MVC는 다양한 뷰 기술(예: JSP, Thymeleaf, FreeMarker 등)을 지원하며, 요청 처리 방식을 유연하게 확장할 수 있다.

 

 

DispatcherServlet이 내부적으로 요청을 처리하는 방식

단계별 요청 처리 과정

  1. 요청 수신
    • 클라이언트의 HTTP 요청이 DispatcherServlet에 전달된다. 이 때, 요청 URL, HTTP 메서드(GET, POST 등), 파라미터 등이 포함된 요청 정보가 전달된다.
  2. HandlerMapping
    • DispatcherServlet은 HandlerMapping을 사용하여 요청을 처리할 적절한 컨트롤러를 찾습니다.
    • HandlerMapping은 URL 패턴과 메서드 정보를 바탕으로 해당 요청을 처리할 컨트롤러와 메서드를 결정한다.
  3. HandlerAdapter
    • HandlerMapping이 반환한 컨트롤러 메서드에 대한 핸들러 어댑터를 사용하여 실제 요청을 처리할 메서드를 실행합니다.
    • 핸들러 어댑터는 다양한 종류의 컨트롤러를 지원할 수 있도록 돕는 역할을 합니다. 예를 들어, @Controller가 선언된 클래스나 @RestController 등을 처리할 수 있다.
  4. 어드바이스 실행 (Advice Execution)
    • Spring AOP(Aspect-Oriented Programming)와 결합된 경우, 요청 전후에 어드바이스가 실행될 수 있다. 예를 들어, 트랜잭션 시작/종료, 로깅 등의 작업이 이 단계에서 수행된다.
  5. 뷰 리졸버(ViewResolver):
    • 컨트롤러가 반환한 모델 데이터를 바탕으로 적절한 뷰를 결정하고, 이를 사용하여 응답을 생성한다. 이때, 뷰 템플릿(예: JSP, Thymeleaf)을 사용하여 최종적인 HTML을 생성한다.
  6. 응답 전송
    • 최종적으로 생성된 응답 HTML을 클라이언트에 전송한다.

+ Recent posts