슬라이딩 윈도우(Sliding Window)는 배열이나 리스트에서 특정 범위(윈도우)에 대한 계산을 반복적으로 수행해야 할 때 유용한 알고리즘 기법
2. 종류
고정 크기 윈도우
주어진 크기의 연속된 구간을 탐색하는 경우
가변 크기 윈도우
특정 조건을 만족하는 구간을 탐색하는 경우
3. 슬라이딩 윈도우 vs 투포인터
3.1 특징
슬라이딩 윈도우
투 포인터
목적
고정되거나 조건에 따라 이동하는 구간(window)을 탐색
두 포인터를 사용해 배열의 특정 조건을 만족하는 값을 탐색
윈도우 크기
고정 또는 가변
일반적으로 가변
주요 동작
윈도우를 한 칸씩 이동하며 필요한 부분만 갱신
두 포인터를 독립적으로 이동하며 구간의 조건을 만족하도록 조정
사용 상황
연속된 구간에 대한 계산이 필요할 때
정렬된 배열에서 특정 조건을 만족하는 값을 찾거나, 구간을 탐색할 때
3.2 공통점
배열이나 리스트에서 효율적으로 탐색
O(N)의 시간 복잡도로 문제 해결 가능
3.3 차이점
탐색 구간
슬라이딩 윈도우
연속된 구간을 유지하며 탐색
투 포인터
두 포인터가 독립적으로 움직이며 조건을 만족하도록 탐색.
조건
슬라이딩 윈도우
윈도우 내 값을 반복적으로 갱신
투 포인터
값이나 조건을 만족할 때까지 포인터를 확장/축소.
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
}
}
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
@MockBean은 Spring 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
@SpyBean은 Spring 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. 주문 상태 변경");
}
}
클린 코드는 단순히 잘 동작하는 코드가 아니라, 가독성, 유지보수성, 확장성이 뛰어난 코드를 의미합니다. 이는 협업과 장기적인 코드 품질을 유지하기 위한 필수적인 개발 철학 입니다.
1.2 클린 코드를 왜 해야 할까?
"이걸 내가 짰다고?"
클린 코드는 미래의 나 혹은 동료에게 친절한 코드
Dirty Code는 폭탄
A를 수정을 하니 B에서 터지네?
협업의 필수 조건
의도가 명확한 코드는 팀워크를 원활하게 만듭니다.
기술 부채는 무섭다
Dirty Code가 쌓이면 나중에 리팩토링이 아니라 재개발이 더 좋을수도….
1.3 리팩터링은 언제 해야 할까?
"이 코드, 이해가 안 돼!"
코드가 읽기 어려운 순간
"여기 고치면 저기 터지네?"
수정할 때마다 오류가 발생한다면
"이거 너무 반복되는 것 같은데?"
같은 코드가 여기저기 복붙되어 있다면?
"새로운 기능 추가가 너무 힘들어!"
확장하려는데 코드 구조가 방해된다면?
"테스트가 너무 어려워!"
단위 테스트를 작성하기 힘들다면?
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);
}
}
}
정리
구체적이고 의도를 담은 이름을 사용
매직 넘버를 피하라
데이터의 의미를 이름에 반영
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.");
}
정리
하나의 함수는 하나의 역할만 수행 (sendEmail은 이메일 발송의 흐름만 관리)
복잡한 작업은 작은 함수로 분리
함수 이름은 동작과 목적을 명확히 표현
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 + "원 전송됨.");
}
정리
주석은 코드가 아닌 의도를 설명
“어떻게”가 아닌 “왜”를 설명
주석 대신 명확한 변수와 함수 이름으로 의도를 드러냄
불필요한 주석은 제거하고, 코드는 가능한 자체적으로 읽히게 작성
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);
}
정리
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();
}
정리
조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리
2.6 부정 표현을 긍정 표현으로 바꾸기
// 나쁜 예
if (!user.isInActive()) {
return "Inactive User";
}
return "Active User";
// 좋은 예
if (user.isActive()) {
return "Active User";
}
return "Inactive User";
정리
긍정적 변수명 사용
긍정적 조건문 작성
이중 부정 지양
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";
}