Hyunsuk's Blog

Hyunsuk's Blog

목표

  • Cursor AI 코딩 도구의 효율적인 활용 방법 학습
  • 복잡한 프로젝트에서 AI와의 협업 프로세스 이해

1. Cursor AI 개요

정의

  • Cursor는 AI 기반 코드 에디터로, 생성형 AI 기술을 활용하여 개발자의 코딩 생산성을 높이는 도구입니다.
  • 실시간 코드 생성, 리팩토링, 버그 수정, 문서화 등 다양한 개발 작업을 AI와 협업하여 수행할 수 있습니다.

주요 특징

  • 실시간 AI 어시스턴트: 코드 작성 중 즉시 AI의 도움을 받을 수 있습니다.
  • 컨텍스트 인식: 프로젝트 전체 구조를 이해하고 맥락에 맞는 제안을 제공합니다.
  • 멀티 모드 지원: Ask 모드와 Agent 모드를 통해 다양한 작업 방식을 지원합니다.

사용 시점

  • 프로토 타입 만들고 싶을 때
  • 코드 리팩토링이나 최적화가 필요할 때
  • 새로운 기술 스택 학습이 필요할 때
  • 반복적인 코딩 작업을 자동화하고 싶을 때

2. Cursor 활용 전략

❌ 비효율적인 사용 방법 (피해야 할 패턴)

⚠️ 경고: 아래 방법들은 Cursor를 비효율적으로 사용하는 예시입니다. 실제 프로젝트에서는 사용하지 마세요!

잘못된 사용 패턴들

1. 무작정 코드 생성 요청
  • “이 기능 만들어줘” - 구체적인 요구사항 없이 요청
  • “버그 고쳐줘” - 문제 상황에 대한 설명 없이 요청
  • “최적화해줘” - 성능 목표나 제약사항 명시 없이 요청
2. 컨텍스트 부족한 요청
  • 프로젝트 구조 설명 없이 코드 요청
  • 기존 코드 스타일이나 아키텍처 고려하지 않음
  • 의존성이나 환경 설정 정보 제공하지 않음
3. 세션 관리 부족
  • 하나의 긴 세션에서 모든 작업 수행
  • 작업 완료 후 커밋하지 않고 계속 진행
  • 이전 대화 내용을 참고하지 않고 새로운 요청
4. 모듈화 고려하지 않음
  • 모든 기능을 하나의 파일에 구현 요청
  • 코드 구조나 설계 패턴 고려하지 않음
  • 확장성이나 유지보수성 고려하지 않음

문제점 분석

  • 비효율성: AI가 맥락을 이해하지 못해 부정확한 결과 도출
  • 시간 낭비: 반복적인 수정과 조정 과정 필요
  • 코드 품질 저하: 구조화되지 않은 코드 생성
  • 유지보수 어려움: 일관성 없는 코드 스타일과 구조

✅ 효율적인 사용 방법

1. 복잡한 기능 구현 프로세스

단계별 접근 방법
1단계: 계획 수립 (Ask 모드 활용)
  • 구현하고자 하는 기능의 구체적인 요구사항 정의
  • 기술 스택과 아키텍처 선택 논의
  • 구현 우선순위와 단계별 계획 수립
2단계: 구현 실행 (Agent 모드 활용)
  • 수립된 계획에 따라 실제 코드 구현
  • 단계별로 검증하고 피드백 반영
  • 테스트 코드와 문서화 포함
3단계: 검증 및 최적화
  • 생성된 코드의 품질 검토
  • 성능 최적화 및 리팩토링
  • 코드 리뷰 및 개선사항 적용

2. 채팅 세션 관리 전략

세션 관리 원칙
1. 작업 단위별 세션 분리
  • 하나의 명확한 작업이 완료되면 새로운 세션 시작
  • 관련 없는 작업들을 하나의 세션에서 처리하지 않음
  • 세션별로 목표와 범위를 명확히 정의
2. 컨텍스트 활용
  • @past chats: 이전 대화 요약 참조
  • @Docs: 공식 문서 및 라이브러리 문서 활용
  • @Git: 특정 브랜치나 커밋 상태 참조
  • @Web/@Link: 외부 리소스 및 참고 자료 활용
  • @Recent change: 최근 변경사항 참조
3. 정기적인 커밋
  • 의미 있는 작업 단위마다 Git 커밋 수행
  • 커밋 메시지에 작업 내용 명확히 기록
  • 브랜치 전략을 활용한 안전한 개발

3. 코드 모듈화 전략

모듈화 접근 방법
1. 아키텍처 설계 (Ask 모드)
  • 기능별 모듈 분리 전략 논의
  • 의존성 관리 방안 수립
  • 인터페이스 설계 및 계약 정의
2. 단계별 구현 (Agent 모드)
  • 핵심 모듈부터 순차적 구현
  • 각 모듈별 테스트 코드 작성
  • 모듈 간 통합 테스트 수행
3. 리팩토링 및 최적화
  • 코드 중복 제거 및 재사용성 향상
  • 성능 최적화 및 메모리 사용량 개선
  • 코드 가독성 및 유지보수성 향상

3. 실제 사용 예시

예시 1: React 컴포넌트 라이브러리 개발

작업 시나리오: 재사용 가능한 UI 컴포넌트 라이브러리 개발

1단계: 계획 수립 (Ask 모드)

사용자: “React로 재사용 가능한 UI 컴포넌트 라이브러리를 만들고 싶어요.
TypeScript를 사용하고, Storybook으로 문서화하고,
테스트 커버리지 80% 이상을 목표로 하고 있어요.”

AI 응답:

  • 컴포넌트 구조 설계 제안
  • 기술 스택 조합 추천
  • 개발 우선순위 및 단계별 계획 수립
  • 아키텍처 패턴 논의
2단계: 구현 실행 (Agent 모드)

사용자: “1단계에서 논의한 Button 컴포넌트부터 구현해주세요.”

AI 실행:

  • TypeScript 인터페이스 정의
  • 컴포넌트 구현
  • 스타일링 (styled-components 또는 CSS-in-JS)
  • 테스트 코드 작성
  • Storybook 스토리 생성
3단계: 검증 및 개선
  • 생성된 코드 리뷰
  • 접근성(a11y) 개선
  • 성능 최적화
  • 문서화 완성

예시 2: API 서버 개발

작업 시나리오: Node.js Express API 서버 개발

1단계: 아키텍처 설계 (Ask 모드)

사용자: “사용자 관리 기능이 있는 REST API를 만들고 싶어요.
JWT 인증, 데이터베이스는 MySQL,
로깅과 에러 핸들링도 포함하고 싶어요.”

AI 응답:

  • 폴더 구조 및 모듈 분리 전략
  • 미들웨어 구성 방안
  • 데이터베이스 스키마 설계
  • 보안 고려사항 및 베스트 프랙티스
2단계: 단계별 구현 (Agent 모드)

사용자: “사용자 등록/로그인 API부터 구현해주세요.”

AI 실행:

  • 데이터베이스 연결 설정
  • 사용자 모델 및 스키마 생성
  • 인증 미들웨어 구현
  • API 엔드포인트 구현
  • 유효성 검사 및 에러 핸들링
  • 테스트 코드 작성
3단계: 통합 및 배포
  • 전체 API 통합 테스트
  • 성능 최적화
  • 환경 설정 관리
  • 배포 스크립트 생성

4. 고급 활용 기법

1. 컨텍스트 주입 전략

효과적인 컨텍스트 활용

@past chats 활용
  • 이전 대화에서 결정된 아키텍처 결정사항 참조
  • 해결된 문제들의 해결책 재사용
  • 프로젝트 진행 상황 파악
@Docs 활용
  • 공식 라이브러리 문서 참조
  • API 사용법 및 베스트 프랙티스 확인
  • 최신 버전 정보 및 변경사항 파악
@Git 활용
  • 특정 브랜치의 코드 상태 참조
  • 이전 구현과의 비교 분석
  • 코드 리뷰 및 변경사항 추적
  • 외부 튜토리얼 및 가이드 참조
  • 커뮤니티 솔루션 및 예시 코드 활용
  • 최신 기술 트렌드 및 베스트 프랙티스 학습

5. 문제 해결 및 트러블슈팅

일반적인 문제와 해결책

자주 발생하는 문제들

1. AI 응답이 부정확한 경우

문제: 생성된 코드가 요구사항과 맞지 않음
해결책:

  • 요구사항을 더 구체적으로 명시
  • 예시 코드나 참고 자료 제공
  • 단계별로 검증하며 진행
2. 코드 품질이 낮은 경우

문제: 생성된 코드가 베스트 프랙티스를 따르지 않음
해결책:

  • 코딩 스타일 가이드 제공
  • 리팩토링 요청
  • 코드 리뷰 및 개선사항 적용
3. 성능 이슈가 있는 경우

문제: 생성된 코드가 성능상 문제가 있음
해결책:

  • 성능 요구사항 명시
  • 최적화 요청
  • 프로파일링 및 개선
4. 의존성 충돌이 있는 경우

문제: 라이브러리 버전 충돌 또는 호환성 문제
해결책:

  • package.json 정보 제공
  • 호환성 검증 요청
  • 대안 라이브러리 제안 요청

6. 베스트 프랙티스 요약

개발 프로세스

  1. 계획 수립: Ask 모드로 구현 계획 논의
  2. 구현 실행: Agent 모드로 실제 코드 구현
  3. 검증 및 개선: 코드 품질 검토 및 최적화
  4. 문서화: 코드 및 API 문서 작성
  5. 테스트: 단위 테스트 및 통합 테스트 수행

세션 관리

  1. 작업 단위별 세션 분리: 명확한 목표를 가진 세션 구성
  2. 컨텍스트 활용: 다양한 컨텍스트 주입으로 정확도 향상
  3. 정기적 커밋: 의미 있는 작업 단위마다 버전 관리
  4. 문서화: 결정사항 및 아키텍처 문서화

코드 품질

  1. 모듈화: 기능별로 적절한 모듈 분리
  2. 테스트: 충분한 테스트 커버리지 확보
  3. 문서화: 코드 주석 및 API 문서 작성
  4. 일관성: 프로젝트 전체의 코딩 스타일 통일

정리

  • Cursor는 AI와의 협업을 통해 개발 생산성을 높여주는 도구라 생각합니다.
  • 적절한 프로세스와 전략을 통해 효율적인 개발이 가능합니다.
  • 물론 코드 생성 완벽하게 믿지는 마세요. 코드 생성 후 반드시 검증과 수정이 필요합니다.

참고 자료

목표

  • 데코레이터 패턴 이해 및 구현

1. 데코레이터 패턴

정의

  • 데코레이터 패턴은 객체에 추가 책임을 동적으로 추가하는 패턴입니다.
  • 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있습니다.
  • 서브클래스 구현보다 유연하게 기능을 확장할 수 있습니다.

사용 시점

  • 기존 코드를 변경하지 않고 새로운 기능을 추가할 때
  • 기존 코드를 재사용하면서 새로운 기능을 추가할 때
  • 런타임에 객체의 기능을 조합하고 싶을 때

클래스 다이어그램

classDiagram
    class Beverage {
        <<abstract>>
        #description: String
        +cost(): double
        +getDescription(): String
    }
    
    class CondimentDecorator {
        <<abstract>>
        #beverage: Beverage
        +cost(): double
        +getDescription(): String
    }
    
    class Espresso {
        +cost(): double
        +getDescription(): String
    }
    
    class HouseBlend {
        +cost(): double
        +getDescription(): String
    }
    
    class DarkRoast {
        +cost(): double
        +getDescription(): String
    }
    
    class Decaf {
        +cost(): double
        +getDescription(): String
    }
    
    class Mocha {
        -beverage: Beverage
        +Mocha(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    class Soy {
        -beverage: Beverage
        +Soy(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    class Whip {
        -beverage: Beverage
        +Whip(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    class SteamedMilk {
        -beverage: Beverage
        +SteamedMilk(beverage: Beverage)
        +cost(): double
        +getDescription(): String
    }
    
    %% 상속 관계
    Beverage <|-- Espresso : extends
    Beverage <|-- HouseBlend : extends
    Beverage <|-- DarkRoast : extends
    Beverage <|-- Decaf : extends
    Beverage <|-- CondimentDecorator : extends
    CondimentDecorator <|-- Mocha : extends
    CondimentDecorator <|-- Soy : extends
    CondimentDecorator <|-- Whip : extends
    CondimentDecorator <|-- SteamedMilk : extends
    
    %% 구성 관계 (Composition)
    CondimentDecorator *-- Beverage : has

2. 예시 코드

❌ 데코레이터 미적용 예시 코드 (문제가 있는 설계)

⚠️ 경고: 아래 코드는 데코레이터 패턴을 적용하지 않은 잘못된 설계입니다. 실제 프로젝트에서는 사용하지 마세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 문제가 있는 설계 - 모든 조합을 위한 클래스가 필요
// 클래스 폭발 문제 발생!
public abstract class Beverage {
protected String description = "Unknown Beverage";
protected boolean milk = false;
protected boolean soy = false;
protected boolean mocha = false;
protected boolean whip = false;

public String getDescription() {
return description;
}

// 복잡하고 유지보수하기 어려운 cost() 메서드
public double cost() {
double condimentCost = 0.0;
if (hasMilk()) {
condimentCost += 0.10;
}
if (hasSoy()) {
condimentCost += 0.15;
}
if (hasMocha()) {
condimentCost += 0.20;
}
if (hasWhip()) {
condimentCost += 0.10;
}
return condimentCost;
}

// 모든 토핑에 대한 getter/setter - 확장성 부족
public boolean hasMilk() { return milk; }
public void setMilk(boolean milk) { this.milk = milk; }
public boolean hasSoy() { return soy; }
public void setSoy(boolean soy) { this.soy = soy; }
public boolean hasMocha() { return mocha; }
public void setMocha(boolean mocha) { this.mocha = mocha; }
public boolean hasWhip() { return whip; }
public void setWhip(boolean whip) { this.whip = whip; }
}

// 모든 조합을 위한 클래스들 - 클래스 폭발 문제!
// 토핑이 4개면 2^4 = 16개의 클래스가 필요!
public class DarkRoastWithMilk extends Beverage {
public DarkRoastWithMilk() {
description = "다크 로스트 커피, 스팀 밀크";
setMilk(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

public class DarkRoastWithMocha extends Beverage {
public DarkRoastWithMocha() {
description = "다크 로스트 커피, 모카";
setMocha(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

public class DarkRoastWithMilkAndMocha extends Beverage {
public DarkRoastWithMilkAndMocha() {
description = "다크 로스트 커피, 스팀 밀크, 모카";
setMilk(true);
setMocha(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

// 더 많은 조합들... 계속해서 클래스가 늘어남
public class DarkRoastWithMilkAndMochaAndWhip extends Beverage {
public DarkRoastWithMilkAndMochaAndWhip() {
description = "다크 로스트 커피, 스팀 밀크, 모카, 휘핑 크림";
setMilk(true);
setMocha(true);
setWhip(true);
}

@Override
public double cost() {
return 0.99 + super.cost();
}
}

// 테스트 코드 - 새로운 토핑 추가 시 모든 조합 클래스 필요
public class StarbuzzCoffeeWithoutDecorator {
public static void main(String[] args) {
// 새로운 토핑이 추가되면 모든 조합의 클래스를 만들어야 함
// 토핑이 4개면 2^4 = 16개의 클래스가 필요!

Beverage beverage1 = new DarkRoastWithMilk();
System.out.println(beverage1.getDescription() + " $" + beverage1.cost());

Beverage beverage2 = new DarkRoastWithMilkAndMocha();
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

// 새로운 토핑(바닐라)이 추가되면?
// DarkRoastWithVanilla, DarkRoastWithMilkAndVanilla,
// DarkRoastWithMochaAndVanilla, DarkRoastWithMilkAndMochaAndVanilla...
// 계속해서 새로운 클래스들이 필요!
}
}

문제점 분석

  • 클래스 폭발: 토핑이 n개면 2^n개의 클래스가 필요
  • 확장성 부족: 새로운 토핑 추가 시 모든 조합의 클래스를 생성해야 함
  • 유지보수 어려움: 각 클래스마다 중복된 코드가 많음
  • 런타임 유연성 부족: 주문 시점에 토핑을 동적으로 조합할 수 없음

✅ 데코레이터 적용 예시 코드

1. 컴포넌트 인터페이스

1
2
3
4
5
6
7
8
9
public abstract class Beverage {
protected String description = "Unknown Beverage";

public String getDescription() {
return description;
}

public abstract double cost();
}

2. 구체적인 음료 클래스들

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Espresso extends Beverage {
public Espresso() {
description = "에스프레소";
}

@Override
public double cost() {
return 1.99;
}
}

public class HouseBlend extends Beverage {
public HouseBlend() {
description = "하우스 블렌드 커피";
}

@Override
public double cost() {
return 0.89;
}
}

public class DarkRoast extends Beverage {
public DarkRoast() {
description = "다크 로스트 커피";
}

@Override
public double cost() {
return 0.99;
}
}

public class Decaf extends Beverage {
public Decaf() {
description = "디카페인 커피";
}

@Override
public double cost() {
return 1.05;
}
}

3. 데코레이터 추상 클래스

1
2
3
4
5
6
7
8
9
10
public abstract class CondimentDecorator extends Beverage {
protected Beverage beverage;

public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}

@Override
public abstract String getDescription();
}

4. 구체적인 데코레이터 클래스들

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 모카";
}

@Override
public double cost() {
return 0.20 + beverage.cost();
}
}

public class Soy extends CondimentDecorator {
public Soy(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 두유";
}

@Override
public double cost() {
return 0.15 + beverage.cost();
}
}

public class Whip extends CondimentDecorator {
public Whip(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 휘핑 크림";
}

@Override
public double cost() {
return 0.10 + beverage.cost();
}
}

public class SteamedMilk extends CondimentDecorator {
public SteamedMilk(Beverage beverage) {
super(beverage);
}

@Override
public String getDescription() {
return beverage.getDescription() + ", 스팀 밀크";
}

@Override
public double cost() {
return 0.10 + beverage.cost();
}
}

5. 테스트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class StarbuzzCoffee {
public static void main(String[] args) {
// 에스프레소 주문
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());

// 다크 로스트 + 모카 + 휘핑 크림 주문
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());

// 하우스 블렌드 + 두유 + 모카 + 휘핑 크림 주문
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());

// 디카페인 + 스팀 밀크 + 모카 주문
Beverage beverage4 = new Decaf();
beverage4 = new SteamedMilk(beverage4);
beverage4 = new Mocha(beverage4);
System.out.println(beverage4.getDescription() + " $" + beverage4.cost());
}
}

실행 결과

1
2
3
4
에스프레소 $1.99
다크 로스트 커피, 모카, 모카, 휘핑 크림 $1.39
하우스 블렌드 커피, 두유, 모카, 휘핑 크림 $1.34
디카페인 커피, 스팀 밀크, 모카 $1.35

데코레이터 패턴 장점

  1. 개방-폐쇄 원칙: 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
  2. 단일 책임 원칙: 각 데코레이터는 하나의 책임만 가집니다.
  3. 유연성: 런타임에 객체의 기능을 조합할 수 있습니다.
  4. 확장성: 새로운 데코레이터를 쉽게 추가할 수 있습니다.
  5. 재사용성: 데코레이터를 다양한 컴포넌트와 조합하여 사용할 수 있습니다.

데코레이터 패턴 단점

  1. 복잡성: 많은 데코레이터가 조합되면 코드가 복잡해질 수 있습니다.
  2. 디버깅 어려움: 데코레이터 체인이 길어지면 디버깅이 어려울 수 있습니다.
  3. 메모리 사용량: 각 데코레이터가 추가 객체를 생성하므로 메모리 사용량이 증가할 수 있습니다.
  4. 구성 초기화 문제: 구성 요소가 많아지면 데코레이터 클래스 구성 요소 초기화가 복잡해질 수 있습니다.

실제 사용 예시

  • Java I/O: InputStream, OutputStream의 데코레이터들 (BufferedInputStream, DataInputStream 등)
  • GUI 컴포넌트: 스크롤바, 테두리, 배경색 등을 동적으로 추가
  • 로깅 시스템: 다양한 로그 레벨과 포맷을 조합
  • 캐싱 시스템: 다양한 캐싱 전략을 조합

정리

  • 데코레이터 패턴은 객체에 추가 책임을 동적으로 추가할 때 사용합니다.
  • 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있습니다.
  • 런타임에 객체의 기능을 조합하고 싶을 때 사용합니다.
  • 개방-폐쇄 원칙을 잘 지키는 패턴입니다.

ref

  • 헤드퍼스트 디자인 패턴

목표

  • 옵저버 패턴 이해 및 구현

1. 옵저버 패턴

정의

  • 옵저버 패턴은 한 객체의 상태가 변경되면 그 객체에 의존하는 다른 객체들에게 연락이 가고 자동으로 내용이 변경되는 일대다 의존성을 정의합니다

사용 시점

  • 객체 간의 일대다 의존성을 정의하고 싶을 때
  • 한 객체의 상태가 변경될 때 다른 객체들이 자동으로 알림을 받고 싶을 때
  • 느슨한 결합을 유지하면서 객체 간 통신이 필요할 때

클래스 다이어그램

classDiagram
    class Subject {
        <<interface>>
        +registerObserver(observer: Observer)
        +removeObserver(observer: Observer)
        +notifyObservers()
    }
    
    class Observer {
        <<interface>>
        +update(temperature: float, humidity: float, pressure: float)
    }
    
    class DisplayElement {
        <<interface>>
        +display()
    }
    
    class WeatherData {
        -observers: List~Observer~
        -temperature: float
        -humidity: float
        -pressure: float
        +registerObserver(observer: Observer)
        +removeObserver(observer: Observer)
        +notifyObservers()
        +measurementsChanged()
        +setMeasurements(temperature: float, humidity: float, pressure: float)
    }
    
    class CurrentConditionsDisplay {
        -weatherData: Subject
        -temperature: float
        -humidity: float
        +CurrentConditionsDisplay(weatherData: Subject)
        +update(temperature: float, humidity: float, pressure: float)
        +display()
    }
    
    class StatisticsDisplay {
        -weatherData: Subject
        -maxTemp: float
        -minTemp: float
        -tempSum: float
        -numReadings: int
        +StatisticsDisplay(weatherData: Subject)
        +update(temperature: float, humidity: float, pressure: float)
        +display()
    }
    
    class ForecastDisplay {
        -weatherData: Subject
        -currentPressure: float
        -lastPressure: float
        +ForecastDisplay(weatherData: Subject)
        +update(temperature: float, humidity: float, pressure: float)
        +display()
    }
    
    %% 인터페이스 구현 관계
    Subject <|.. WeatherData : implements
    Observer <|.. CurrentConditionsDisplay : implements
    Observer <|.. StatisticsDisplay : implements
    Observer <|.. ForecastDisplay : implements
    DisplayElement <|.. CurrentConditionsDisplay : implements
    DisplayElement <|.. StatisticsDisplay : implements
    DisplayElement <|.. ForecastDisplay : implements
    
    %% 구성 관계 (Composition)
    WeatherData *-- Observer : has
    
    %% 의존 관계 (Dependency)
    CurrentConditionsDisplay --> Subject : observes
    StatisticsDisplay --> Subject : observes
    ForecastDisplay --> Subject : observes

예시 코드

기상 스테이션과 다양한 디스플레이를 구현한 예시를 통해 옵저버 패턴을 살펴보겠습니다.

1. 주제(Subject) 인터페이스

1
2
3
4
5
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}

2. 옵저버(Observer) 인터페이스

1
2
3
public interface Observer {
void update(float temperature, float humidity, float pressure);
}

3. 디스플레이 인터페이스

1
2
3
public interface DisplayElement {
void display();
}

4. 기상 데이터 클래스 (주제)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;

public WeatherData() {
observers = new ArrayList<>();
}

@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}

@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}

@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}

public void measurementsChanged() {
notifyObservers();
}

public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}

5. 현재 조건 디스플레이 (옵저버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private Subject weatherData;
private float temperature;
private float humidity;

public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}

@Override
public void display() {
System.out.println("현재 조건: " + temperature + "F 도, " + humidity + "% 습도");
}
}

6. 통계 디스플레이 (옵저버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class StatisticsDisplay implements Observer, DisplayElement {
private Subject weatherData;
private float maxTemp = 0.0f;
private float minTemp = 200.0f;
private float tempSum = 0.0f;
private int numReadings = 0;

public StatisticsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

@Override
public void update(float temperature, float humidity, float pressure) {
tempSum += temperature;
numReadings++;

if (temperature > maxTemp) {
maxTemp = temperature;
}

if (temperature < minTemp) {
minTemp = temperature;
}

display();
}

@Override
public void display() {
System.out.println("평균/최고/최저 온도 = " + (tempSum / numReadings) + "/" + maxTemp + "/" + minTemp);
}
}

7. 예보 디스플레이 (옵저버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ForecastDisplay implements Observer, DisplayElement {
private Subject weatherData;
private float currentPressure = 29.92f;
private float lastPressure;

public ForecastDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

@Override
public void update(float temperature, float humidity, float pressure) {
lastPressure = currentPressure;
currentPressure = pressure;
display();
}

@Override
public void display() {
System.out.print("예보: ");
if (currentPressure > lastPressure) {
System.out.println("날씨가 좋아지고 있습니다!");
} else if (currentPressure == lastPressure) {
System.out.println("날씨가 그대로입니다.");
} else if (currentPressure < lastPressure) {
System.out.println("비가 올 것 같습니다.");
}
}
}

8. 테스트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();

CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);

System.out.println("=== 첫 번째 측정 ===");
weatherData.setMeasurements(80, 65, 30.4f);

System.out.println("\n=== 두 번째 측정 ===");
weatherData.setMeasurements(82, 70, 29.2f);

System.out.println("\n=== 세 번째 측정 ===");
weatherData.setMeasurements(78, 90, 29.2f);
}
}

실행 결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== 첫 번째 측정 ===
현재 조건: 80.0F 도, 65.0% 습도
평균/최고/최저 온도 = 80.0/80.0/80.0
예보: 날씨가 좋아지고 있습니다!

=== 두 번째 측정 ===
현재 조건: 82.0F 도, 70.0% 습도
평균/최고/최저 온도 = 81.0/82.0/80.0
예보: 비가 올 것 같습니다.

=== 세 번째 측정 ===
현재 조건: 78.0F 도, 90.0% 습도
평균/최고/최저 온도 = 80.0/82.0/78.0
예보: 날씨가 그대로입니다.

옵저버 패턴 장점

  1. 느슨한 결합: 주제와 옵저버가 느슨하게 결합되어 있습니다.
  2. 확장성: 새로운 옵저버를 쉽게 추가할 수 있습니다.
  3. 일대다 관계: 하나의 주제가 여러 옵저버에게 알림을 보낼 수 있습니다.
  4. 자동 알림: 주제의 상태가 변경되면 모든 옵저버가 자동으로 알림을 받습니다.

정리

  • 옵저버 패턴은 객체 간의 일대다 의존성을 정의할 때 사용합니다.
  • 한 객체의 상태가 변경될 때 다른 객체들이 자동으로 알림을 받고 싶을 때 사용합니다.
  • 느슨한 결합을 유지하면서 객체 간 통신이 필요할 때 사용합니다.

ref

  • 헤드퍼스트 디자인 패턴

목표

  • 컴포지션 사용

1. 컴포지션

사용 시점

  • 객체 간의 관계를 “has-a” 관계로 표현하고 싶을 때
  • 런타임에 객체의 동작을 변경하고 싶을 때
  • 상속 대신 유연한 구조가 필요할 때

클래스 다이어그램

classDiagram
    class WeaponBehavior {
        <<interface>>
        +useWeapon()
    }
    
    class Character {
        #weaponBehavior: WeaponBehavior
        +setWeaponBehavior(wb: WeaponBehavior)
        +performWeapon()
        +display()*
    }
    
    class Knight {
        +display()
    }
    
    class Archer {
        +display()
    }
    
    class SwordBehavior {
        +useWeapon()
    }
    
    class BowBehavior {
        +useWeapon()
    }
    
    WeaponBehavior <|.. SwordBehavior : implements
    WeaponBehavior <|.. BowBehavior : implements
    Character <|-- Knight : extends
    Character <|-- Archer : extends
    Character *-- WeaponBehavior : has

예시 코드

게임 캐릭터와 무기 시스템을 구현한 예시를 통해 컴포지션을 살펴보겠습니다.

1. 무기 행동 인터페이스

1
2
3
public interface WeaponBehavior {
void useWeapon();
}

2. 구체적인 무기 행동 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 검 사용
public class SwordBehavior implements WeaponBehavior {
@Override
public void useWeapon() {
System.out.println("검으로 베기 공격!");
}
}

// 활 사용
public class BowBehavior implements WeaponBehavior {
@Override
public void useWeapon() {
System.out.println("활로 화살 발사!");
}
}

3. 캐릭터 추상 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class Character {
protected WeaponBehavior weaponBehavior;

public void setWeaponBehavior(WeaponBehavior wb) {
this.weaponBehavior = wb;
}

public void performWeapon() {
if (weaponBehavior != null) {
weaponBehavior.useWeapon();
} else {
System.out.println("무기가 없습니다!");
}
}

public abstract void display();
}

4. 구체적인 캐릭터 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 기사 클래스
public class Knight extends Character {
public Knight() {
// 기본적으로 검을 장착
weaponBehavior = new SwordBehavior();
}

@Override
public void display() {
System.out.println("나는 기사입니다!");
}
}

// 궁수 클래스
public class Archer extends Character {
public Archer() {
// 기본적으로 활을 장착
weaponBehavior = new BowBehavior();
}

@Override
public void display() {
System.out.println("나는 궁수입니다!");
}
}

5. 테스트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Game {
public static void main(String[] args) {
// 기사 생성
Character knight = new Knight();
knight.display();
knight.performWeapon();

// 궁수 생성
Character archer = new Archer();
archer.display();
archer.performWeapon();

// 무기 교체
System.out.println("기사가 활로 무기를 교체합니다!");
knight.setWeaponBehavior(new BowBehavior());
knight.performWeapon();

System.out.println("궁수가 검으로 무기를 교체합니다!");
archer.setWeaponBehavior(new SwordBehavior());
archer.performWeapon();
}
}

실행 결과

1
2
3
4
5
6
7
8
9
10
나는 기사입니다!
검으로 베기 공격!
나는 궁수입니다!
활로 화살 발사!

기사가 활로 무기를 교체합니다!
활로 화살 발사!

궁수가 검으로 무기를 교체합니다!
검으로 베기 공격!

컴포지션 장점

  1. 유연성: 런타임에 무기를 교체할 수 있습니다.
  2. 확장성: 새로운 무기나 캐릭터를 쉽게 추가할 수 있습니다.
  3. 재사용성: 무기 행동을 다른 클래스에서도 재사용할 수 있습니다.
  4. 캡슐화: 무기 관련 동작이 WeaponBehavior 인터페이스에 캡슐화되어 있습니다.

정리

  • 컴포지션은 객체 간의 관계를 “has-a” 관계로 표현하고 싶을 때 사용합니다.
  • 상속 대신 유연한 구조가 필요할 때 사용합니다.
  • 런타임에 객체의 동작을 변경하고 싶을 때 사용합니다.
  • 상속 대신 유연한 구조가 필요할 때 사용합니다.

ref

  • 헤드퍼스트 디자인 패턴

목표

리팩토링 스터디

리팩터링:예시 - chapter 1

1. 리팩터링이란?

리팩터링은 코드의 외부 동작은 그대로 유지한 채, 코드 구조를 개선하는 작업입니다. 마치 집을 리모델링하는 것처럼, 기존 기능은 그대로 유지하면서 내부 구조를 개선하는 것입니다.

2. 리팩터링이 필요한 이유

  • 코드의 가독성 향상
  • 버그 수정 용이성 증가
  • 새로운 기능 추가가 쉬워짐
  • 코드 재사용성 향상
  • 유지보수 비용 감소

3. 주요 리팩터링 기법

3.1 변수 관련 리팩터링

  • 변수 이름 바꾸기

    • 명확한 이름으로 변경하는 것을 주저하지 않기
    • IDE의 리팩터링 도구 활용 권장
  • 임시 변수 처리

    • 임시 변수는 질의 함수로 변경
    • 값이 바뀌지 않는 변수는 인라인화
    • 사용하는 변수는 바로 앞단에 위치시키기

3.2 함수 관련 리팩터링

  • 반복문 쪼개기

    • 기능별로 for문 분리
    • 주의사항: 성능 영향 확인 필요
      • 컴파일러 캐싱 기법으로 인한 성능 영향
      • 실제 실행 환경에서 테스트 필요
  • 문장 슬라이드

    • 관련 코드를 가까이 배치
    • 임시 변수를 질의 함수로 변경하기 쉽게 만듦

3.3 구조 개선 리팩터링

  • 단계 쪼개기

    • 계산 단계와 표현 단계 분리
    • 각 단계별 책임 명확화
  • 생성자를 팩터리 함수로 변경

    • JavaScript의 생성자 제한 해결
    • 서브클래스 인스턴스 반환 가능
  • 조건부 로직을 다형성으로 변경

    • 서브클래스별 로직 분리
    • 슈퍼클래스는 추상 메서드만 정의
    • 서브클래스에서 구체적 구현

3.4 데이터 처리 리팩터링

  • 얕은 복사 활용
    • 가변 데이터를 불변 데이터로 사용
    • 부작용 방지

4. 리팩터링 진행 단계

  1. 중첩 함수 분리

    • 여러 개의 독립적인 함수로 분리
    • 상위 함수 변수 접근 가능
  2. 단계 쪼개기

    • 계산 코드와 출력 코드 분리
    • 각 단계의 책임 명확화
  3. 다형성 적용

    • 계산 로직을 다형성으로 표현
    • 확장성과 유지보수성 향상

5. 좋은 코드의 기준

  • 수정하기 쉬운 코드
  • 이해하기 쉬운 코드
  • 재사용하기 쉬운 코드
  • 테스트하기 쉬운 코드

6. 리팩터링 시 주의사항

  • 테스트 코드 작성 필수
  • 작은 단계로 나누어 진행
  • 각 단계마다 테스트 실행
  • 성능 영향 고려

레퍼런스

  • 리팩터링2판
0%