[JAVA] 좋은 코드란 무엇일까? (SRP/OCP, map/flatMap)

nbh·2025년 10월 12일

JAVA

목록 보기
2/2

"일단 돌아가게만 만들자!"고 생각하며 작성한 코드는, 몇 달 뒤 돌아보면 나조차도 이해하기 힘든 거대한 스파게티 덩어리가 되어 있었다. 새로운 기능을 하나 추가하려면 수많은 파일을 수정해야 했고, 예상치 못한 곳에서 버그가 발생했다.

"수정"

이라는 단어가 두려워지는 순간이었다.
이러한 문제를 해결하기 위해 객체지향 설계 원칙과 효율적인 데이터 처리 방법을 학습했다. 그 과정에서 마주한 핵심 개념이 바로 SOLID 원칙Stream API였다.

1. 좋은 설계의 첫걸음 - SOLID 원칙

복잡한 코드 속에서 길을 잃지 않게 도와주는 5개의 원칙, SOLID. 그중 가장 기본이 되는 두 가지를 먼저 정리해 보았다.

A. 단일 책임 원칙 (SRP: Single Responsibility Principle) - 하나의 책임만!

SRP는 하나의 클래스는 단 하나의 책임만 가져야 한다는 원칙이다. 만약 Employee 클래스가 직원의 정보를 관리하면서, 동시에 데이터베이스에 저장하고, 급여를 계산하는 책임까지 모두 가지고 있다면 어떨까?

// SRP 위반 예시
public class Employee {
    private String name;
    private long salary;

    // 책임 1: 정보 관리
    public String getName() { /* ... */ }
    public long getSalary() { /* ... */ }

    // 책임 2: 데이터베이스 처리
    public void saveToDatabase() { /* ... */ }

    // 책임 3: 급여 계산
    public long calculatePay() { /* ... */ }
}

이 경우, 급여 계산 방식이 바뀌거나 데이터베이스 종류가 바뀌어도 Employee 클래스는 계속 수정되어야 했다. 너무 많은 책임을 가지고 있었기 때문이다.

언제 적용할까?

  • 클래스의 역할이 비대해지고 복잡해질 때
  • 어떤 변경이 필요할 때, 수정해야 할 이유가 하나 이상으로 보일 때
  • 코드의 재사용성과 테스트 용이성을 높이고 싶을 때

책임을 분리하면 코드가 명확해지고, 변경의 영향 범위가 줄어든다.

B. 개방-폐쇄 원칙 (OCP: Open-Closed Principle) - 확장은 쉽게, 변경은 어렵게!

OCP는 소프트웨어의 구성요소(클래스, 모듈 등)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙이다. 새로운 기능이 추가되더라도 기존 코드는 수정되지 않아야 한다는 의미다.

예를 들어, 결제 유형에 따라 다른 로직을 처리해야 할 때 if-else로 분기하는 코드가 있었다.

// OCP 위반 예시
public class PaymentProcessor {
    public void process(Payment payment) {
        if (payment.getType().equals("CREDIT_CARD")) {
            // 신용카드 결제 로직
        } else if (payment.getType().equals("BANK_TRANSFER")) {
            // 계좌이체 결제 로직
        }
        // 새로운 결제 수단이 추가될 때마다 이 코드를 '변경'해야 한다!
    }
}

이 코드는 새로운 결제 수단이 추가될 때마다 PaymentProcessor 클래스를 직접 수정해야 했다. 변경에 닫혀있지 않은 구조였다.

언제 적용할까?

  • 새로운 기능이나 정책이 자주 추가될 것으로 예상될 때
  • 핵심 로직의 변경 없이 기능을 유연하게 확장하고 싶을 때
  • 다형성을 활용하여 코드의 유연성을 높이고자 할 때

OCP를 지키면 기존 코드를 건드리지 않고도 새로운 기능을 안전하게 추가할 수 있다.

2. 데이터를 자유자재로 - map vs flatMap

복잡한 데이터를 다룰 때 for문과 if문을 중첩해서 사용하면 코드의 가독성이 떨어지기 쉬웠다. Java 8의 Stream API, 특히 mapflatMap은 데이터를 우아하게 처리하는 강력한 도구였다.

A. map - 1:1로 변환하기

map은 스트림의 각 요소를 받아서 다른 요소로 변환하는, 가장 직관적인 연산이다. 입력 요소 하나당 출력 요소 하나가 나오는 1:1 매핑이다.

List<String> words = Arrays.asList("apple", "banana", "cherry");

// 각 단어를 대문자로 변환
List<String> upperCaseWords = words.stream()
                                   .map(String::toUpperCase)
                                   .collect(Collectors.toList());
// 결과: ["APPLE", "BANANA", "CHERRY"]

언제 사용할까?

  • 스트림의 각 요소를 다른 값이나 객체로 일대일 매핑하고 싶을 때
  • 특정 필드만 추출하거나, 데이터의 형태를 바꾸고 싶을 때

B. flatMap - 포장지를 벗겨 하나로 합치기

flatMap은 각 요소를 변환하여 새로운 스트림을 만들고, 이 모든 스트림들을 하나의 평평한 스트림으로 이어붙이는 연산이다. 중첩된 구조를 단일 계층으로 평탄화할 때 유용하다.

// 문장 배열을 단어 목록으로 만들기
String[] sentences = {"hello world", "java stream"};

// map을 사용하면 List<String[]> 형태의 중첩 구조가 나온다.
List<String[]> wordsArray = Arrays.stream(sentences)
                                  .map(s -> s.split(" "))
                                  .collect(Collectors.toList());
// 결과: [["hello", "world"], ["java", "stream"]]

// flatMap을 사용하면 모든 단어가 포함된 단일 리스트가 나온다.
List<String> uniqueWords = Arrays.stream(sentences)
                                   .flatMap(s -> Arrays.stream(s.split(" ")))
                                   .collect(Collectors.toList());
// 결과: ["hello", "world", "java", "stream"]

언제 사용할까?

  • 하나의 입력 요소에서 여러 개의 출력 요소가 나올 수 있을 때
  • 중첩된 리스트나 배열 구조를 단일 리스트로 평탄화하고 싶을 때

정리하며

처음부터 완벽한 설계를 하기는 어렵다. 하지만 코드를 작성할 때 이런 원칙들을 떠올리는 것만으로도 코드의 질은 달라질 수 있다.

  1. SRP: "이 클래스가 너무 많은 일을 하고 있진 않나?"
  2. OCP: "기능을 추가할 때 기존 코드를 바꾸지 않을 방법은 없을까?"
  3. map/flatMap: "이 for문을 더 간결하게 바꿀 순 없을까?"

처음에는 복잡해 보이지만, 각 개념의 목적을 이해하고 상황에 맞게 사용하다 보면 자연스럽게 익숙해진다.
새로운 배열의 키보드를 익힐 때처럼, 몸에 익을 때까지 계속 적용해가며 체화할 것이다.

0개의 댓글