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 변환 + 평탄화
'코드잇 BE스프린트' 카테고리의 다른 글
| 위클리 페이퍼 - 4주차 (0) | 2026.03.31 |
|---|---|
| 위클리 페이퍼 - 3주차 (1) | 2026.03.17 |
| 위클리 페이퍼 - 1주차 (1) | 2026.03.16 |
| 코드잇 스프린트 고급 프로젝트(Mopl) 개발 회고록 (0) | 2025.12.17 |
| 알림 조회 시 겪은 타임존 변환 문제와 해결과정 (0) | 2025.12.15 |