728x90

1. 정의

슬라이딩 윈도우(Sliding Window)는 배열이나 리스트에서 특정 범위(윈도우)에 대한 계산을 반복적으로 수행해야 할 때 유용한 알고리즘 기법

2. 종류

  • 고정 크기 윈도우
    • 주어진 크기의 연속된 구간을 탐색하는 경우
  • 가변 크기 윈도우
    • 특정 조건을 만족하는 구간을 탐색하는 경우

3. 슬라이딩 윈도우 vs 투포인터

3.1 특징

  슬라이딩 윈도우 투 포인터
목적 고정되거나 조건에 따라 이동하는 구간(window)을 탐색 두 포인터를 사용해 배열의 특정 조건을 만족하는 값을 탐색
윈도우 크기 고정 또는 가변 일반적으로 가변
주요 동작 윈도우를 한 칸씩 이동하며 필요한 부분만 갱신 두 포인터를 독립적으로 이동하며 구간의 조건을 만족하도록 조정
사용 상황 연속된 구간에 대한 계산이 필요할 때 정렬된 배열에서 특정 조건을 만족하는 값을 찾거나, 구간을 탐색할 때

3.2 공통점

  1. 배열이나 리스트에서 효율적으로 탐색
  2. O(N)의 시간 복잡도로 문제 해결 가능

3.3 차이점

  1. 탐색 구간
    • 슬라이딩 윈도우
      • 연속된 구간을 유지하며 탐색
    • 투 포인터
      • 두 포인터가 독립적으로 움직이며 조건을 만족하도록 탐색.
  2. 조건
    • 슬라이딩 윈도우
      • 윈도우 내 값을 반복적으로 갱신
    • 투 포인터
      • 값이나 조건을 만족할 때까지 포인터를 확장/축소.

3.4 선택 조건

  • 슬라이딩 윈도우를 선택:
    • 구간 내 합, 곱, 최대/최소값을 반복적으로 계산해야 하는 경우.
    • 문제에서 “연속된 서브배열”이라는 조건이 있는 경우.
      • 예: 크기가 K인 서브배열의 최대합.
  • 투 포인터를 선택:
    • 정렬된 배열에서 조건을 만족하는 두 값이나 범위를 찾는 경우.
    • 문제에서 “특정 합”, “차이”, “곱” 등을 만족하는 두 요소를 요구하는 경우.
      • 예: 두 수의 합, 특정 값의 구간 찾기.

4. 예시

4.1 고정 크기 윈도우

import java.util.Arrays;

public class FixedWindowAverage {

    public static double[] findAverages(int[] arr, int k) {
        if (arr.length < k) {
            throw new IllegalArgumentException("Array length must be greater than or equal to k.");
        }

        double[] result = new double[arr.length - k + 1];
        double windowSum = 0;

        // 초기 윈도우 합 계산
        for (int i = 0; i < k; i++) {
            windowSum += arr[i];
        }

        result[0] = windowSum / k;

        // 슬라이딩 윈도우 이동
        for (int i = k; i < arr.length; i++) {
            windowSum += arr[i] - arr[i - k]; // 새 요소 추가, 오래된 요소 제거
            result[i - k + 1] = windowSum / k; // 평균 계산
        }

        return result;
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        int k = 3;

        double[] averages = findAverages(arr, k);
        System.out.println("크기 " + k + "의 서브배열 평균값: " + Arrays.toString(averages));
        // 출력: [2.0, 3.0, 4.0]
    }
}

4.2 가변 크기 윈도우

public class VariableWindowExample {

    public static int minSubArrayLen(int target, int[] arr) {
        int n = arr.length;
        int left = 0, sum = 0;
        int minLength = Integer.MAX_VALUE;

        for (int right = 0; right < n; right++) {
            sum += arr[right]; // 윈도우 확장

            while (sum >= target) { // 조건 만족 시 윈도우 축소
                minLength = Math.min(minLength, right - left + 1);
                sum -= arr[left];
                left++;
            }
        }

        return minLength == Integer.MAX_VALUE ? 0 : minLength;
    }

    public static void main(String[] args) {
        int[] arr = {2, 3, 1, 2, 4, 3};
        int target = 7;

        int result = minSubArrayLen(target, arr);
        System.out.println("합이 " + target + " 이상인 가장 짧은 서브배열의 길이: " + result); // 출력: 2
    }
}
반응형

'Study > Algorithm' 카테고리의 다른 글

[Algorithm] 이분탐색  (0) 2024.12.20
[Algorithm] 탐욕법(그리디) 알고리즘(greedy algorithm)  (0) 2021.01.13
728x90

💡 TDD(Test-Driven Development): 실패에서 시작하는 개발의 예술

1. TDD(Test-Driven Development)

🎯1.1 TDD란 무엇인가요?

  • TDD는 개발자의 ‘습관’
    • “테스트부터 작성하자. 그리고 통과시키자!”
  • 정의: 테스트를 먼저 작성하고, 그 테스트를 통과하기 위해 코드를 작성하는 개발 방법론.
  • 왜 중요한가요?
    • 실패를 두려워하지 않고, 더 나은 코드를 만들어 가는 과정!
    • Red-Green-Refactor라는 단순하지만 강력한 루틴으로 코드 품질을 높임.

🚦 1.2 기본 사이클: Red-Green-Refactor

1.2.1 Red: 실패를 즐기자!

  • 테스트부터 작성 (아직 구현은 X).
  • 실패를 통해 요구사항과 문제를 명확히 이해.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CalculatorTest {

    @Test
    void testAddition() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result); // 실패! 왜? 구현 코드가 없으니까.
    }
}

1.2.2 Green: 테스트 통과는 최소 노력으로.

  • 테스트를 통과시키는 코드 작성.
  • 복잡하게 고민하지 말고 “빨리 테스트 통과”에 집중!
public class Calculator {
    public int add(int a, int b) {
        return a + b; // 통과! 왜? 테스트가 요구한 기능만 구현했으니까.
    }
}

1.2.3 Refactor: 이제 진짜 멋지게!

  • 읽기 좋은 코드, 유지보수하기 쉬운 코드로 개선.
  • 코드의 동작은 그대로, 구조는 깔끔하게!
public class Calculator {
    // 향후 확장을 고려해 더 나은 구조로 개선
    public int add(int a, int b) {
        return calculate(a, b, "+");
    }

    // 공통 로직 처리 메서드 추가
    private int calculate(int a, int b, String operation) {
        switch (operation) {
            case "+":
                return a + b;
            default:
                throw new UnsupportedOperationException("Operation not supported");
        }
    }
}

✨ 1.3 왜 중요한가요?

  • 🛡 안정성: 모든 기능에 대한 자동화된 테스트 보장.
  • 📈 생산성 향상: 리팩토링 부담 감소.

“TDD를 하면 시간 낭비라고?” 아니요! 미래의 당신이 감사할 겁니다. 😎

🔥 1.4 장단점

  • 장점
    • 코드의 신뢰성 증가.
    • 요구사항 변경에도 흔들리지 않는 튼튼한 구조.
    • 버그 감소로 디버깅 시간 절약.
  • 단점
    • 초기 진입장벽 존재.
    • 테스트 작성에 시간 투자 필요.
    • 잘못된 테스트 설계 시 오히려 발목을 잡을 수 있음.

🙀 1.5 DDD(Domain Driven Design)와의 관계

  • TDD는 테스트를 통해 코드를 어떻게 구현할지에 초점을 둔 개발 방법론
  • DDD는 도메인 모델을 설계하여 무엇을 구현할지를 정의하는 철학
질문 TDD DDD
왜 사용하는가? 기능의 동작을 보장하기 위해 비즈니스 요구사항을 시스템에 반영하기 위해
왜 시작하는가? 코드를 테스트하면서 명확한 피드백을 얻기 위해 요구사항과 도메인 모델을 기반으로 설계하기 위해
 중요한가? 코드의 안정성과 품질을 높이기 위해 복잡한 비즈니스 로직을 제대로 반영하기 위해
어떻게 다른가? 테스트 작성 → 코드 작성 → 리팩토링 요구사항 분석 → 도메인 설계 → 코드 작성

2. 테스트 코드

💡2.1 테스트 코드란 무엇인가요?

  • “내 코드, 정말 제대로 작동할까?”라는 질문에 대한 답을 미리 준비하는 개발자의 무기
  • 프로그램의 기능이 의도대로 동작하는지 자동으로 확인

🛠 2.2 종류

2.2.1 단위 테스트 (Unit Test)

  • 특징: 순수 비즈니스 로직(단일 메서드나 클래스)을 테스트
  • 도구: @Mock, Mockito.
  • 예시: UserService가 UserRepository를 올바르게 호출하는지 테스트
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Optional;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    private UserService userService;

    @Test
    void should_ReturnUser_When_ValidIdProvided() {
        // Mock 초기화
        MockitoAnnotations.openMocks(this);
        userService = new UserService(userRepository);

        // Given
        User mockUser = new User(1L, "Alice");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));

        // When
        User result = userService.getUserById(1L);

        // Then
        assertEquals("Alice", result.getName());
        verify(userRepository, times(1)).findById(1L);
    }
}

2.2.2 통합 테스트 (Integration Test)

  • 특징: 여러 계층(Service, Repository 등)이 올바르게 연동되는지 확인
  • 도구: @SpringBootTest
  • 예시: 데이터 저장 및 조회가 잘 동작하는지 확인.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional // 테스트 후 DB 롤백
class UserIntegrationTest {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Test
    void should_SaveAndRetrieveUser() {
        // Given
        User newUser = new User(null, "Bob");

        // When
        userService.saveUser(newUser);
        User retrievedUser = userService.getUserById(newUser.getId());

        // Then
        assertNotNull(retrievedUser);
        assertEquals("Bob", retrievedUser.getName());
    }
}

2.2.3 End-to-End 테스트 (E2E Test)

  • 특징: 시스템의 전체 흐름이 사용자 관점에서 잘 작동하는지 검증
  • 도구: @SpringBootTest, TestRestTemplate
  • 예시: 사용자 생성 후 조회 API가 올바르게 동작하는지 확인.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserE2ETest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void should_CreateAndRetrieveUser() {
        // Given
        String baseUrl = "http://localhost:" + port + "/users";
        UserRequest userRequest = new UserRequest("Charlie");

        // Step 1: 사용자 생성
        UserResponse createdUser = restTemplate.postForObject(baseUrl, userRequest, UserResponse.class);

        assertNotNull(createdUser);
        assertEquals("Charlie", createdUser.getName());

        // Step 2: 생성된 사용자 조회
        UserResponse retrievedUser = restTemplate.getForObject(baseUrl + "/" + createdUser.getId(), UserResponse.class);

        assertNotNull(retrievedUser);
        assertEquals("Charlie", retrievedUser.getName());
    }
}

🚦 2.3 Spring Boot 테스트에서 사용하는 Mock의 종류

2.3.1 @Mock

  • @Mock은 객체의 동작을 정의해서 의존성을 가짜로 만들어 줍니다.
  • 주로 단위 테스트에서 사용하며, Mock 라이브러리인 Mockito를 함께 사용합니다.
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    private UserRepository userRepository; // 독립된 Mock 객체 생성

    private UserService userService;

    @Test
    void should_ReturnUser_When_ValidIdProvided() {
        // Mock 초기화
        MockitoAnnotations.openMocks(this);
        userService = new UserService(userRepository);

        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Alice")));

        // When
        User result = userService.getUserById(1L);

        // Then
        assertEquals("Alice", result.getName());
        verify(userRepository, times(1)).findById(1L);
    }
}

2.3.2 @MockBean

  • @MockBeanSpring Context에 등록된 빈(bean)을 Mock 객체로 바꿔줍니다.
  • 덕분에 Controller, Service, Repository가 연동된 통합 테스트에서도 유용합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import java.util.Optional;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @MockBean
    private OrderRepository orderRepository; // Repository 빈을 Mock으로 대체

    @Test
    void should_ReturnOrder_When_OrderExists() {
        // Given
        Order mockOrder = new Order(1L, "ProductA", 3, "CREATED");
        when(orderRepository.findById(1L)).thenReturn(Optional.of(mockOrder));

        // When
        Order result = orderService.getOrderById(1L);

        // Then
        assertNotNull(result);
        assertEquals("ProductA", result.getProductName());
        verify(orderRepository, times(1)).findById(1L);
    }
}

2.3.3 @Spy

  • @Spy는 실제 객체를 사용하면서 특정 메서드만 Mocking할 수 있습니다.
  • 로직의 일부만 제어하고 싶을 때 유용합니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Spy;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    @Spy
    private OrderService orderService = new OrderService();

    @Test
    void should_MockSpecificMethod() {
        // Given
        doReturn(500).when(orderService).calculateTotalPrice(100, 5);

        // When
        int totalPrice = orderService.calculateTotalPrice(100, 5);
        String status = orderService.getOrderStatus(1L); // 실제 메서드 실행

        // Then
        assertEquals(500, totalPrice); // Mocking된 결과 검증
        assertEquals("COMPLETED", status); // 실제 동작 검증

        // Verify
        verify(orderService, times(1)).calculateTotalPrice(100, 5);
        verify(orderService, times(1)).getOrderStatus(1L);
    }
}

2.3.4 @SpyBean

  • @SpyBeanSpring Context에 등록된 실제 빈Spy 객체로 감싸줍니다.
  • 즉, Spring 빈의 실제 동작을 수행하면서 일부 메서드만 Mocking 할 수 있습니다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @SpyBean
    private OrderService spyOrderService;

    @Test
    void should_UseSpyBeanToMockSpecificMethod() {
        // Given
        doReturn(1000).when(spyOrderService).calculateTotalPrice(200, 5);

        // When
        int mockedResult = spyOrderService.calculateTotalPrice(200, 5); // Mock 동작
        String status = spyOrderService.getOrderStatus(1L); // 실제 동작 실행

        // Then
        assertEquals(1000, mockedResult); // Mocking 결과
        assertEquals("COMPLETED", status); // 실제 메서드 결과

        // Verify
        verify(spyOrderService, times(1)).calculateTotalPrice(200, 5);
        verify(spyOrderService, times(1)).getOrderStatus(1L);
    }
}
어노테이션 대상 설명 사용 예
@Mock 독립된 가짜 객체 Spring Context와 무관, 가짜 객체 생성 순수 단위 테스트
@MockBean Spring 빈을 가짜로 대체 Spring Context에서 Mock 객체로 교체 Controller와 Service 연결 테스트
@Spy 실제 객체 (부분만 Mocking) 일부 메서드만 Mocking, 나머지는 실제 실행 실제 객체의 부분 테스트
@SpyBean Spring 빈을 Spy 객체로 감싸기 Spring 빈을 감싸서 일부만 Mocking 실제 Spring 빈의 동작과 호출 검증

✨ 2.4 테스트코드를 구조적으로 깔끔하게 짜는 꿀팁

2.4.1 @Nested

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("OrderService 단위 테스트")
class OrderServiceTest {

    private OrderService orderService = new OrderService();

    @Nested
    @DisplayName("주문 생성 테스트")
    class CreateOrderTests {

        @Test
        @DisplayName("정상적으로 주문을 생성한다.")
        void should_CreateOrder_Successfully() {
            Order order = orderService.createOrder("ProductA", 3);
            assertNotNull(order);
            assertEquals("ProductA", order.getProductName());
        }
    }

    @Nested
    @DisplayName("주문 조회 테스트")
    class GetOrderTests {

        @Test
        @DisplayName("존재하는 주문을 조회한다.")
        void should_ReturnOrder_When_OrderExists() {
            Order order = orderService.createOrder("ProductA", 3);
            Order retrievedOrder = orderService.getOrderById(order.getId());
            assertEquals(order, retrievedOrder);
        }
    }
}

2.4.2 @DisplayName

  • @DisplayName은 테스트의 목적이나 동작을 설명하는 이름을 추가
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("OrderService 테스트")
class OrderServiceTest {

    private OrderService orderService = new OrderService();

    @Test
    @DisplayName("주문 생성 시 상품 이름과 수량이 올바르게 설정된다.")
    void should_CreateOrder_With_CorrectDetails() {
        Order order = orderService.createOrder("ProductA", 3);
        assertNotNull(order);
        assertEquals("ProductA", order.getProductName());
        assertEquals(3, order.getQuantity());
    }
}

2.4.3 @BeforeEach / @AfterEach

  • @BeforeEach: 각 테스트 실행 전 공통 작업을 수행
  • @AfterEach: 각 테스트 실행 후 정리 작업을 수행
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    private OrderService orderService;

    @BeforeEach
    void setUp() {
        orderService = new OrderService();
    }

    @Test
    @DisplayName("주문 생성 시 상품 이름이 설정된다.")
    void should_CreateOrder_With_ProductName() {
        Order order = orderService.createOrder("ProductA", 3);
        assertEquals("ProductA", order.getProductName());
    }

    @AfterEach
    void tearDown() {
        System.out.println("테스트 정리 작업 실행");
    }
}

2.4.4 @TestMethodOrder + @Order

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;

import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(OrderAnnotation.class) // @Order 어노테이션 기준으로 순서 지정
@DisplayName("OrderService 순서 지정 테스트")
class OrderServiceTest {

    private static OrderService orderService;

    @BeforeAll
    static void setUp() {
        orderService = new OrderService();
    }

    @Test
    @Order(1)
    @DisplayName("1. 주문을 생성한다.")
    void testCreateOrder() {
        Order order = orderService.createOrder("ProductA", 3);
        assertNotNull(order);
        assertEquals("ProductA", order.getProductName());
    }

    @Test
    @Order(2)
    @DisplayName("2. 주문을 조회한다.")
    void testGetOrder() {
        Order order = orderService.getOrderById(1L); // 1번 ID의 주문 조회
        assertNotNull(order);
        assertEquals("ProductA", order.getProductName());
    }

    @Test
    @Order(3)
    @DisplayName("3. 주문 상태를 변경한다.")
    void testUpdateOrderStatus() {
        Order updatedOrder = orderService.updateOrderStatus(1L, "COMPLETED");
        assertEquals("COMPLETED", updatedOrder.getStatus());
    }
}

2.4.5 @TestMethodOrder(MethodName.class)

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.MethodName;

import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodName.class) // 메서드 이름 순서대로 실행
class OrderServiceTest {

    @Test
    void a_testCreateOrder() {
        System.out.println("1. 주문 생성");
    }

    @Test
    void b_testGetOrder() {
        System.out.println("2. 주문 조회");
    }

    @Test
    void c_testUpdateOrderStatus() {
        System.out.println("3. 주문 상태 변경");
    }
}
반응형

'Study > etc...' 카테고리의 다른 글

소프트웨어 아키텍처  (0) 2025.01.03
클린 코드  (1) 2024.12.13
728x90

1. 클린 코드란 무엇인가

1.1 정의

클린 코드는 단순히 잘 동작하는 코드가 아니라, 가독성, 유지보수성, 확장성이 뛰어난 코드를 의미합니다.
이는 협업과 장기적인 코드 품질을 유지하기 위한 필수적인 개발 철학 입니다.

1.2 클린 코드를 왜 해야 할까?

  1. "이걸 내가 짰다고?"
    • 클린 코드는 미래의 나 혹은 동료에게 친절한 코드
  2. Dirty Code는 폭탄
    • A를 수정을 하니 B에서 터지네?
  3. 협업의 필수 조건
    • 의도가 명확한 코드는 팀워크를 원활하게 만듭니다.
  4. 기술 부채는 무섭다
    • Dirty Code가 쌓이면 나중에 리팩토링이 아니라 재개발이 더 좋을수도….

1.3 리팩터링은 언제 해야 할까?

  1. "이 코드, 이해가 안 돼!"
    • 코드가 읽기 어려운 순간
  2. "여기 고치면 저기 터지네?"
    • 수정할 때마다 오류가 발생한다면
  3. "이거 너무 반복되는 것 같은데?"
    • 같은 코드가 여기저기 복붙되어 있다면?
  4. "새로운 기능 추가가 너무 힘들어!"
    • 확장하려는데 코드 구조가 방해된다면?
  5. "테스트가 너무 어려워!"
    • 단위 테스트를 작성하기 힘들다면?

2. 클린 코드의 기본 원칙

2.1 의미 있는 이름 짓기

// 나쁜 예
public void processData(List<String> data) {
    for (String item : data) {
        if (item.length() > 5) {
            System.out.println(item);
        }
    }
}

// 좋은 예
public void printLongUserNames(List<String> userNames) {
    final int MIN_NAME_LENGTH = 5;
    for (String userName : userNames) {
        if (userName.length() > MIN_NAME_LENGTH) {
            System.out.println(userName);
        }
    }
}
  • 정리
    1. 구체적이고 의도를 담은 이름을 사용
    2. 매직 넘버를 피하라
    3. 데이터의 의미를 이름에 반영

2.2 함수 분리 하기

// 나쁜 예
public void sendEmail(String recipient, String subject, String body) {
    if (recipient == null || recipient.isEmpty()) {
        throw new IllegalArgumentException("Recipient cannot be null or empty");
    }
    System.out.println("Connecting to SMTP server...");
    System.out.println("Authenticating...");
    System.out.println("Sending email to: " + recipient);
    System.out.println("Subject: " + subject);
    System.out.println("Body: " + body);
    System.out.println("Email sent successfully.");
}

// 좋은 예
public void sendEmail(String recipient, String subject, String body) {
    validateRecipient(recipient);
    connectToSmtpServer();
    authenticate();
    deliverEmail(recipient, subject, body);
}

private void validateRecipient(String recipient) {
    if (recipient == null || recipient.isEmpty()) {
        throw new IllegalArgumentException("Recipient cannot be null or empty");
    }
}

private void connectToSmtpServer() {
    System.out.println("Connecting to SMTP server...");
}

private void authenticate() {
    System.out.println("Authenticating...");
}

private void deliverEmail(String recipient, String subject, String body) {
    System.out.println("Sending email to: " + recipient);
    System.out.println("Subject: " + subject);
    System.out.println("Body: " + body);
    System.out.println("Email sent successfully.");
}
  • 정리
    1. 하나의 함수는 하나의 역할만 수행 (sendEmail은 이메일 발송의 흐름만 관리)
    2. 복잡한 작업은 작은 함수로 분리
    3. 함수 이름은 동작과 목적을 명확히 표현

2.3 불필요한 주석 제거

// 나쁜 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
    // 송금 금액이 0보다 커야 합니다.
    if (amount <= 0) {
        throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
    }

    // 잔액 확인
    if (fromAccount.getBalance() < amount) {
        throw new IllegalStateException("계좌 잔액이 부족합니다.");
    }

    // 같은 계좌인지 확인
    if (fromAccount.equals(toAccount)) {
        throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
    }

    // 송금 실행
    fromAccount.withdraw(amount); // 돈을 출금합니다.
    toAccount.deposit(amount);    // 돈을 입금합니다.

    // 송금 로그
    System.out.println("송금 성공: " + amount + "원 전송됨.");
}

// 좋은 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
    // 비즈니스 규칙: 송금 금액은 0보다 커야 함
    if (amount <= 0) {
        throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
    }

    // 비즈니스 규칙: 송금 계좌 잔액이 부족하면 송금 불가
    if (fromAccount.getBalance() < amount) {
        throw new IllegalStateException("계좌 잔액이 부족합니다.");
    }

    // 비즈니스 규칙: 동일 계좌 간 송금 금지 (실수 방지 목적)
    if (fromAccount.equals(toAccount)) {
        throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
    }

    // 송금 실행
    fromAccount.withdraw(amount);
    toAccount.deposit(amount);

    // 로그 기록: 성공적인 송금을 기록 (보안 및 추적 목적)
    System.out.println("송금 성공: " + amount + "원 전송됨.");
}
  • 정리
    1. 주석은 코드가 아닌 의도를 설명
      • “어떻게”가 아닌 “왜”를 설명
    2. 주석 대신 명확한 변수와 함수 이름으로 의도를 드러냄
    3. 불필요한 주석은 제거하고, 코드는 가능한 자체적으로 읽히게 작성

2.4 코드 중복 제거

// 나쁜 예
public void printUserName(String name) {
    System.out.println("User: " + name);
}

public void printAdminName(String name) {
    System.out.println("Admin: " + name);
}

// 좋은 예
public void printName(String role, String name) {
    System.out.println(role + ": " + name);
}
  • 정리
    1. DRY (Don’t Repeat Yourself) 원칙을 준수

2.5 복잡한 코드를 단순화하기

// 나쁜 예
if (user != null && user.getAge() > 18 && user.isActive()) {
    // do something
}

// 좋은 예
if (isActiveAdultUser(user)) {
    // do something
}

private boolean isActiveAdultUser(User user) {
    return user != null && user.getAge() > 18 && user.isActive();
}
  • 정리
    1. 조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리

2.6 부정 표현을 긍정 표현으로 바꾸기

// 나쁜 예
if (!user.isInActive()) {
    return "Inactive User";
}
return "Active User";

// 좋은 예
if (user.isActive()) {
    return "Active User";
}
return "Inactive User";
  • 정리
    1. 긍정적 변수명 사용
    2. 긍정적 조건문 작성
    3. 이중 부정 지양

2.7 else 문 사용 지양하기

// 나쁜 예
public void login(User user) {
    if (user != null) {
        if (user.isActive()) {
            if (user.isVerified()) {
                System.out.println("Login successful");
            } else {
                System.out.println("User is not verified");
            }
        } else {
            System.out.println("User is inactive");
        }
    } else {
        System.out.println("Invalid user");
    }
}

// 좋은 예
public void login(User user) {
    if (user == null) {
        System.out.println("Invalid user");
        return;
    }
    if (!user.isActive()) {
        System.out.println("User is inactive");
        return;
    }
    if (!user.isVerified()) {
        System.out.println("User is not verified");
        return;
    }

    // 모든 조건을 통과한 경우
    System.out.println("Login successful");
}

// 더 좋은 예
public void login(User user) {
    String validationResult = validateUser(user);

    if (!validationResult.equals("Valid")) {
        System.out.println(validationResult);
        return;
    }

    // 모든 조건을 통과한 경우
    System.out.println("Login successful");
}

private String validateUser(User user) {
    if (user == null) {
        return "Invalid user";
    }
    if (!user.isActive()) {
        return "User is inactive";
    }
    if (!user.isVerified()) {
        return "User is not verified";
    }

    return "Valid";
}
  • 정리
    1. else 문을 피하고 기본 동작을 명시
    2. 전처리와 핵심 로직을 분리
    3. 각 조건은 독립적으로 처리

참고

반응형

'Study > etc...' 카테고리의 다른 글

소프트웨어 아키텍처  (0) 2025.01.03
TDD(Test-Driven Development)  (1) 2024.12.18

+ Recent posts