[Weekly Paper] SRP, OCP와 Stream API의 map과 flatMap

Jerry·2025년 7월 14일

Questions

Q. 객체지향 프로그래밍에서 '단일 책임 원칙(SRP)'과 '개방-폐쇄 원칙(OCP)'에 대해 설명하고, 각각의 원칙을 적용한 코드 예시를 들어주세요.

A.

단일 책임 원칙 (SRP: Single Responsibility Principle)

정의

  • 하나의 클래스(혹은 모듈)는 단 하나의 책임(변화의 이유)만 가져야 합니다.
  • 즉, 클래스는 오직 하나의 기능(역할)에만 집중해야 하며, 여러 가지 일을 동시에 맡으면 안 됩니다.

장점

  • 변경 시 영향 범위가 작아져 유지보수가 쉬움
  • 기능 분리가 명확하여 테스트 및 확장 용이
  • 재사용성과 가독성 향상

예시

// bad case
public class Report {
    public String generateReport() {
        // 보고서 생성 로직
        return "report content";
    }

    public void saveToFile(String content) {
        // 파일 저장 로직
    }
}
// 한 클래스가 보고서 생성과 파일 저장 두 가지 책임을 가짐 → SRP 위반

// good case
public class ReportGenerator {
    public String generateReport() {
        // 보고서 생성 로직
        return "report content";
    }
}

public class FileSaver {
    public void saveToFile(String content) {
        // 파일 저장 로직
    }
}
// 두 클래스가 각자 한 가지 책임만 가짐 → SRP 준수

개방-폐쇄 원칙 (OCP: Open-Closed Principle)

정의

  • 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다.
  • 즉, 새로운 기능이 필요할 때 코드를 수정하지 말고, 확장(상속, 인터페이스 등)으로 구현해야합니다.

장점

  • 기존 코드에 대한 안정성 확보 (기능 추가로 인한 오류 방지)
  • 새로운 요구사항에 유연하게 대응 가능
  • 유지보수가 쉬움

예시

// bad case
public class ReportExporter {
    public void export(String content, String format) {
        if (format.equals("PDF")) {
            System.out.println("Exporting as PDF: " + content);
        } else if (format.equals("HTML")) {
            System.out.println("Exporting as HTML: " + content);
        } else {
            throw new IllegalArgumentException("지원하지 않는 포맷입니다.");
        }
    }
}
// 새로운 포맷을 추가하려면 if-else에 코드를 계속 추가해야 함 → OCP 위반

// good case
public interface Exporter {
    void export(String content);
}

public class PdfExporter implements Exporter {
    public void export(String content) {
        System.out.println("Exporting as PDF: " + content);
    }
}

public class HtmlExporter implements Exporter {
    public void export(String content) {
        System.out.println("Exporting as HTML: " + content);
    }
}

public class ReportExporter {
    public void export(String content, Exporter exporter) {
        exporter.export(content);
    }
}
// ReportExporter는 Exporter 인터페이스에만 의존
// 새로운 포맷이 필요하면 기존 코드 수정 없이 Exporter 구현체만 추가하면 확장됨 → OCP 준수

Q. Stream API의 map과 flatMap의 차이점을 설명하고, 각각의 활용 사례를 예시 코드와 함께 설명해주세요.

A.

Stream API란?

  • Stream API(java.util.stream)는 Java 8부터 추가된 기능으로, 컬렉션을 쓸 때 함수형 프로그래밍 스타일로 데이터를 변환(map), 필터링(filter), 집계(reduce), 정렬(sorted), 평탄화(flatMap) 등 다양한 연산을 할 수 있게 해주는 라이브러리입니다.
  • Stream은 데이터 소스를 직접 변경하지 않고, "데이터의 흐름"을 선언적으로 기술할 수 있게 해줍니다.
  • 파이프라인 전체가 최종 연산 시점에 한꺼번에 실행되어 lazy evaluation 방식으로 동작합니다.

map

  • map(Function<T, R>) 각 요소를 1:1로 변환
  • 소스 코드
    @Override
    public <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                         StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<P_OUT, R>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        downstream.accept(mapper.apply(u));
                    }
                };
            }
        };
    }

flatMap

  • flatMap(Function<T, Stream<R>> 각 요소를 Stream<R>로 변환 후 모든 Stream을 하나로 평탄화(flatten): map + flatten
  • n차원이면 n-1번 flatMap 해야함
  • flatMap(inner -> inner.stream())로 flatten만 하는 경우가 많음
  • 소스 코드
    @Override
    public <R> Stream<R> flatMap(Function<? super P_OUT, ? extends Stream<? extends R>> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                         StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<P_OUT, R>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        try (Stream<? extends R> result = mapper.apply(u)) {
                            if (result != null)
                                result.sequential().forEach(downstream::accept);
                        }
                    }
                };
            }
        };
    }

map과 flatMap의 차이점

구분mapflatMap
변환1:1 변환 (입력 1개 → 출력 1개)1:N 변환 (입력 1개 → 0개~N개로 펼침)
함수T → RT → Stream<R> 또는 T → IntStream 등
결과Stream<R> (중첩 스트림이 있을 수 있음)Stream<R> (중첩 스트림을 평탄화)
사용일반적인 값 변환내부가 리스트, 배열 등으로 이루어져 있어 평탄화가 필요한 경우
계층계층 구조 유지 (예: 2차원 → 2차원)계층 구조 제거 (예: n차원 → n - 1 차원)

예시 코드와 활용 사례

  • map 활용 사례: 문자열 리스트 → char[] 리스트로 변환

    List<String> words = Arrays.asList("a", "bb", "ccc");
    
    List<char[]> mapped = words.stream()
        .map(String::toCharArray) // String → char[]
        .collect(Collectors.toList());
    
    for (char[] arr : mapped) {
        System.out.println(Arrays.toString(arr));
    }
    // 출력:
    // [a]
    // [b, b]
    // [c, c, c]
  • flatMap 활용 사례: List<List<String>>(2차원) → 1차원 리스트로 평탄화

    List<List<String>> list2d = Arrays.asList(
        Arrays.asList("a", "bb"),
        Arrays.asList("ccc", "dddd")
    );
    
    List<String> flatMapped = list2d.stream()
        .flatMap(Collection::stream)
        .collect(Collectors.toList());
    
    System.out.println(flatMapped)
    // 결과: [a, bb, ccc, dddd]
profile
Backend engineer

0개의 댓글