[SB 3기] 코드잇 스프린트 위클리페이퍼 2주차

JHLee·2025년 4월 13일
post-thumbnail

[SB 3기] 코드잇 스프린트 위클리페이퍼 2주차

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

1. 단일 책임 원칙 (SRP)

  • 단일 책임 원칙(Single Responsibility Principle)이란, 하나의 클래스는 '하나의 책임'만 가져야 한다는 원칙이다.
    - 여기서 책임이란 하나의 기능/역할을 의미한다.
    - 클래스는 오직 한 가지의 변경 이유만 가져야 한다.

"하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다."
- 이는 SOLID 원칙을 창시한 로버트C마틴이 한 말이다.
- 이때 액터(Actor)는 한 명일수도 있고, 여러 명이 될 수도 있다.

예를 들어, 음악 스트리밍 사이트에서는 음악을 재생하는 로직과 음악을 다운로드하는 로직 등이 서로 다른 책임이므로 분리하는 것이 SRP를 적용하는 것이다.

장점

  • 코드 가독성 향상
  • 유지보수에 용이
  • 모듈화 향상
  • 다른 객체와의 의존성/연관성 최소화

단점

  • 기능의 수대로 클래스의 수가 증가하면, 코드의 길이가 길어질 수 있음

💡 예시 코드

// 음악 재생 책임
public class SongPlayer {
    public void play(String title) {
        System.out.println(title + "을 재생합니다.");
    }
}

// 음악 저장 책임
public class SongStorage {
    public void saveToFile(String title) {
        System.out.println(title + "을 파일에 저장합니다.");
    }
}

2. 개방-폐쇄 원칙 (OCP)

  • 개방-폐쇄 원칙(Open-Closed Principle)이란, "확장에 열려 있고, 변경에는 닫혀있어야 한다." 는 원칙이다.
    - "확장에 열려있다" : 새로운 기능 추가 등으로 기능을 확장할 수 있어야 한다.
    - "변경에는 닫혀있다" : 기존 코드는 최대한 수정하지 않아야 한다.
    - 즉 소프트웨어는 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.
  • ⭐️ 수정을 많이 하면, 오류 발생 확률이 높아지고, 변경에 의한 추가 비용이 발생한다. (테스트 비용, 배포 비용 등..) 유지보수가 어려워진다.
  • 변경이 일어날 수 있는 부분과 그렇지 않은 부분을 명확히 정의해야 한다.
  • 이를 위해선, 추상화와 다형성을 적극 활용해야 한다.
  • 인터페이스나 추상 클래스를 정의하여, 변경 필요한 부분은 이를 구현하여 새로운 클래스로 정의하여 기능을 확장한다.
public interface PlayList {
	public void playSong(String title);
}

public class kpopSong implements PlayList {
	public void playSong(String title) {
		System.out.println("(KPOP) "+title+"을 플레이합니다.");
	}
}

public class jpopSong implements PlayList {
	public void playSong(String title) {
		System.out.println("(JPOP) "+title+"을 플레이합니다.");
	}
}

// 추후 기능 확장
public class popSong implements PlayList {
	public void playSong(String title) {
		System.out.println("(POP) "+title+"을 플레이합니다.");
	}
}

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

map

  • map() 메서드는 스트림의 각 요소를 1:1로 변환(매핑)하는 중간연산이다.
  • 이는 주로 객체에서 원하는 요소를 추출하거나, 다른 형태로 변환이 필요할 때 사용한다.
  • 반환한 결과는 새로운 스트림으로 이어진다.

💡 예시 코드

List<List<Integer>> nestedList = Arrays.asList(
			Arrays.asList(1,2),
			Arrays.asList(3,4)
		);
        
// map()
nestedList.stream()
          .map(List::stream) 
          .forEach(e -> e.forEach(i -> System.out.print(i + " "))); // 두 번째 forEach로 내부 스트림을 순회

// 1 2 3 4

flatMap

  • flatMap() 메서드는 중첩된 리스트를 펼쳐서 하나의 평면적인 스트림으로 펼쳐주는 중간 연산이다.(평면화) (1:N 매핑)
  • 이는 중첩된 컬렉션을 단일 스트림으로 만들어 중첩 구조를 단순화 할 수 있다.
  • 입력한 원소를 가장 작은 단위의 단일 스트림으로 반환시킨다.

	List<List<Integer>> nestedList = Arrays.asList(
		Arrays.asList(1,2),
		Arrays.asList(3,4)
	);

    // flatMap()
	nestedList.stream()
    		  .flatMap(List::stream)
              .forEach(e -> System.out.print(e + " "));
	//1 2 3 4
	
  • flatMap은 결과를 스트림으로 반환하기 때문에 flatMap의 결과를 가지고 바로 forEach 메서드를 체이닝하여 모든 요소를 출력할 수 있다.
  • flatMap은 내부적으로 여러 스트림을 병합하는 과정이 필요하므로, map()에 비해 메모리 사용량이 더 많을 수 있다.
  • 중첩된 컬렉션을 다룰땐 flatMap()을 쓰는것이 가독성과 효율면에서 더 좋다.
profile
렛츠고

0개의 댓글