코드잇 BE스프린트

위클리 페이퍼 - 2주차

beginner-development 2026. 3. 17. 00:08

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

'단일 책임 원칙(SRP)'

  • 핵심 개념
    • 하나의 클래스(또는 모듈)는 단 하나의 변경 이유만 가져야 함.
    • 클래스는 하나의 역할(기능) 에만 집중해야 한다.

'개방-폐쇄 원칙(OCP)'

  • 핵심 개념
    • 새로운 기능은 기존 코드를 변경하지 않고 확장으로 구현해야 한다.
    • 코드를 수정하지 않고도 기능을 추가할 수 있어야 안정적인 시스템 구축이 가능하다.

요약 정리

원칙 의미 목적 핵심 전략
SRP(단일 책임) 하나의 클래스는 하나의 책임만 가져야 한다. 변경 이유 분리, 응집도 상승 기능을 모듈별로 분리
OCP(개방-폐쇄) 확장에는 열려 있고 변경에는 닫혀 있어야 한다. 안정적 확장 가능 인터페이스, 다형성 활용

 

 

SRP 예시

  • SRP 위반
    • 모든 기능이 한 클래스에 몰려 있다.
public class PostService {

    public void createPost(String title, String content, String email) {
        // 1. 게시글 저장
        System.out.println("Saving post: " + title);

        // 2. 이메일 발송
        System.out.println("Sending email to " + email);
    }
}
  • SRP 적용
    • 클래스마다 책임이 분리되어 변경에 강해지고, 테스트/유지보수가 쉬워짐.
// 게시글 관련 책임
public class PostService {
    private final PostRepository postRepository;
    private final NotificationService notificationService;

    public PostService(PostRepository postRepository, NotificationService notificationService) {
        this.postRepository = postRepository;
        this.notificationService = notificationService;
    }

    public void createPost(String title, String content, String email) {
        postRepository.save(title, content);
        notificationService.sendEmail(email, "게시글이 등록되었습니다.");
    }
}

// 데이터 저장 책임
public class PostRepository {
    public void save(String title, String content) {
        System.out.println("DB 저장: " + title);
    }
}

// 알림 전송 책임
public class NotificationService {
    public void sendEmail(String to, String message) {
        System.out.println("이메일 전송: " + to + " - " + message);
    }
}

 

OCP 예시

  • OCP 위반
    • 알림 방식이 늘어날 때마다 NotificationService 수정 필요 → OCP 위반
public class NotificationService {
public void send(String type, String to, String message) {
  if (type.equals("EMAIL")) {
    System.out.println("이메일 전송: " + to + " - " + message);
    } else if (type.equals("SMS")) {
       System.out.println("SMS 전송: " + to + " - " + message);
   }
 }
}

 

 

  • OCP 적용: 다형성 + 인터페이스 기반 설계
        - 새로운 SlackNotifier 같은 알림 방식 추가 시, 기존 코드 건드릴 필요 없음
// 1. 인터페이스 정의
public interface Notifier {
    void send(String to, String message);
}

// 2. 구현체들
public class EmailNotifier implements Notifier {
    public void send(String to, String message) {
        System.out.println("이메일 전송: " + to + " - " + message);
    }
}

public class SmsNotifier implements Notifier {
    public void send(String to, String message) {
        System.out.println("SMS 전송: " + to + " - " + message);
    }
}

// 3. 서비스는 Notifier만 의존함 (OCP 적용)
public class NotificationService {
    private final List<Notifier> notifiers;

    public NotificationService(List<Notifier> notifiers) {
        this.notifiers = notifiers;
    }

    public void notifyAll(String to, String message) {
        for (Notifier notifier : notifiers) {
            notifier.send(to, message);
        }
    }
}
  • 사용 예
public class App {
    public static void main(String[] args) {
        List<Notifier> notifiers = List.of(new EmailNotifier(), new SmsNotifier());
        NotificationService notificationService = new NotificationService(notifiers);

        notificationService.notifyAll("user@example.com", "게시글이 등록되었습니다.");
    }
}

 

 

  • 두 원칙의 적용 방식 및 실전 적용 효과 정리
원칙 적용 방식 실전 적용 효과
SRP 기능을 클래스/서비스 별로 나눔 클래스 하나당 변경 이유 1개 → 유지보수 쉬움
OCP 인터페이스 기반 확장 구조 새 기능 추가 시 기존 코드 수정 없음

 

나만의 언어로 정리해보기

단일 책임 원칙(SRP)
하나의 클래스는 하나의 역할에만 집중해야한다! 기능을 모듈별로 분리하여 변경 이유를 분리하고 응집도를 상승시킨다.
개방-폐쇄 원칙(OCP)
확장에는 열림! 변경에는 닫힘! 인터페이스, 다형성을 활용하여 안정적인 확장을 가능하게 한다.

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

  • map과 flatMap은 Java Stream API에서 데이터 변환에 자주 사용되는 메서드이지만, 리턴 구조의 차이로 인해 쓰임새가 다르다.

1. map() - 1:1 변환

  • 각 요소를 하나의 값으로 변환 (transform)
  • 예제: 이름을 대문자로 변환
List<String> names = List.of("alice", "bob", "charlie");
List upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());

System.out.println(upperNames); // [ALICE, BOB, CHARLIE]
  • 각 "alice" → "ALICE": 1 → 1
  • map()은 한 요소 → 한 값으로 매핑됨

 

 

2. flatMap() - 1:N 변환 + 평탄화

  • 각 요소가 여러 값(Stream)으로 바뀔 때, 중첩 구조를 평탄화(flatten)
  • 예제: 문자열 리스트를 단어로 분리
List<String> sentences = List.of("hello world", "java stream");

List words = sentences.stream()  
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))  
.collect(Collectors.toList());

System.out.println(words); // [hello, world, java, stream]
  • map()을 썼다면 Stream<Stream<String>>이 됨 → 중첩 스트림
  • flatMap()은 Stream<String>로 평탄화(flatten) 해줌

 

  • 구조 차이 시각화
입력 요소 map() 결과 flatMap() 결과
"a b" Stream<["a", "b"]> "a", "b"
"c d" Stream<["c", "d"]> "c", "d"
전체 Stream<Stream<String>> Stream<String>

 

 

  • 사용 상황
상황 사용 메서드
하나의 값 → 하나의 다른 값 map()
하나의 값 → 여러 값(리스트, 배열, 스트림 등) flatMap()

 

  • 사용 목적
목적 추천 메서드
요소 하나를 다른 값으로 변환 map()
요소 하나를 여러 값으로 변환하고 전개 flatMap()
중첩 리스트를 풀어 하나의 리스트로 flatMap()
DTO 변환, 필드 추출 map()

 

 

1. map() 활용 사례: DTO 변환, 값 추출, 값 변경
  • 사례: Entity → DTO 변환
    class User {
      private String name;
      private int age;
      // getters, constructor 생략
    }
    class UserDto {
    	private String name;
        
        public UserDto(String name) {
        	this.name = name;
       }
    }
    
    List users = List.of(
    	new User("Alice", 25),
    	new User("Bob", 30)
    );
    
    List dtos = users.stream()
    	.map(user -> new UserDto(user.getName())) // 1:1 변환
    	.collect(Collectors.toList());
  • User → UserDto로 하나의 객체를 하나의 DTO로 변환

 

  • 사례: 숫자 제곱 계산
List<Integer> numbers = List.of(1, 2, 3, 4);`

List squares = numbers.stream()  
.map(n -> n \* n) // 1:1 변환  
.collect(Collectors.toList());

// 결과: \[1, 4, 9, 16\]
  • 각 숫자를 제곱한 값으로 단일 매핑

 

 

2. flatMap() 활용 사례: 다중 요소 평탄화, 중첩 리스트 해제
  • 사례: 문장 → 단어로 분해
    List<String> sentences = List.of("hello world", "java stream api");
    List words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" "))) // String[] → Stream
    .collect(Collectors.toList());
    
    // 결과: [hello, world, java, stream, api]
  • "hello world" → "hello", "world"
  • 각 문장을 쪼개고 전체를 평탄화

 

사례: List<List<Integer>> → List<Integer>

List<List<Integer>> nestedList = List.of( 
	List.of(1, 2), 
    List.of(3, 4), 
    List.of(5) 
);

List flatList = nestedList.stream()  
	.flatMap(List::stream)  
	.collect(Collectors.toList());

// 결과: \[1, 2, 3, 4, 5\]
  • 중첩 리스트를 하나의 리스트로 풀어냄

 

사례: 유저별 주문 내역 전체 집계

class Order { 
	private String item;
    // constructor 생략
}

class User {
	private String name;
    private List<Order> orders;
    // constructor 생략
}

List users = List.of(
	new User("Alice", List.of(new Order("apple"), new Order("banana"))),
	new User("Bob", List.of(new Order("carrot")))
);

List allOrders = users.stream()
	.flatMap(user -> user.getOrders().stream()) // User → Stream
	.collect(Collectors.toList());
  • 모든 유저의 주문 목록을 한꺼번에 모을 때 유용

 

차이점 요약

  • map()은 → List<String[]> (중첩)
  • flatMap()은 → List<String> (평탄화)

 

나만의 언어로 정리해보기

Map
이름을 대문자로 변환하는 것처럼 각 요소를 하나의 값으로 변환한다. DTO 변환, 필드 추출 등에 사용된다.
1:1 변환
FlatMap
문자열 리스트를 단어로 분리하는 것처럼 각 요소가 여러 개의 값으로 바뀔 때, 중첩 구조를 평탄화한다. 중첩 리스트를 풀어 하나의 리스트로 만들 때 사용한다.
1:N 변환 + 평탄화