포큐 아카데미 COMP 2500 기말고사 대비

2026.08.15. 17:07

기말고사 대비 주제

  1. 상속 vs 컴포지션

    • 상속 : 자식 클래스가 부모 클래스를 상속 받는 관계

    • 컴포지션 : 한 클래스가 다른 클래스의 객체를 멤버 변수(필드)로 가지고 있는 관계

    • 재사용성의 이점

      • 설계와 코딩을 다시 할 필요가 없음

      • 테스트에 걸리는 시간을 절약

      • 관리비용 절약

    • 상속과 컴포지션 둘 다 재사용성이 목적

    • 가능/ 불가능의 측면에서만 보면 많은 경우에 둘 다 사용 가능

    • 둘 중 하나를 고를 원칙이 필요

      1. 기계 상의 차이 때문에 하나를 골라야 할 때

        1. 상속 시 부모 개체와 자식 개체가 하나의 덩어리
          Box box = new Box(10, 20, 30);

        2. 컴포지션에선 메모리가 여러 개의 덩어리.
          Rectangle rectangle = new Rectangle(10,20);
          Box box = new Box(rectangle, 30);

        3. new 할 때마다 메모리를 할당함.

          • cpu와 메모리 사이의 데이터 전송(버스) 병목 -> 캐시 메모리 사용

          • 상속 - 개체가 한 번에 캐시 메모리에 들어갈 가능성이 높음.

          • 컴포지션 - 개체 내 부품 수 만큼 캐시 메모리로 로딩할 가능성이 높음.

        4. 새로운 메모리 할당과 해체

          • 이 둘 중 하나는 특히 느림.

          • 상속은 메모리 할당과 해제가 딱 한 번씩

          • 컴포지션은 한 번 + 부품 수만큼씩.
            성능 : 상속 > 컴포지션

      2. 용도 때문에 상속을 고를 수 밖에 없을 때

        • 다형성을 사용해야할 때 - 다른 형의 개체들을 한꺼번에 처리하고 싶을 때

      3. 관리의 효율성을 고려할 때

        • 상속 이점 : 컴포지션 관계에서 클래스 멤버변수의 호출 함수를 별도로 만들어야 하는 경우 - 상속이면 부모에서 하나만 만들어두면 됨.

        • 컴포지션 이점 : 깊은 상속 문제. 상위 클래스를 바꾸면 그 아래 클래스도 모두 바뀜. 자식 클래스에서 문제가 없는지 모두 확인해야함. - 컴포지션도 문제는 있지만 상속보다는 덜함. 상속보다 조립성을 더 강조했기 때문.

      4. 그 외 일반적인 상황

        • 코딩 표준, 규약, 합의 등을 따를 것

        • has-a 관계면 컴포지션, is-a 관계면 상속

  2. 상속이 유리한 경우

    1. 기계의 동작 상 상속이 유리한 경우

      • 상속의 경우 메모리가 하나의 블럭으로 들어감. 컴포지션은 내부에서 필드로 갖고 있는 것만큼 나눠져 있음. -> 캐시메모리 로딩 시 한 번에 들어가게 되어 상속이 유리

      • 메모리 할당과 해제 시 둘 중 하나는 느릴 수 밖에 없고 여러 번 new()를 호출해야하는 경우보다 메모리를 한 덩어리로 갖고 있는 상속이 유리

    2. 용도 때문에 상속을 고를 수 밖에 없을 때

      • 다형성을 구현해야 하는 경우 (for 문 등으로 클래스들의 함수를 일괄 호출해야하는 경우)

    3. 관리 효율성 고려 시

      • 클래스 멤버변수의 기능과 중복이 있는 경우 (ex 클래스 멤버변수의 호출 함수가 중복되게 있어야 하는 경우) - 코드의 중복이 있는 경우 상속이 유리

  3. 컴포지션이 유리한 경우

    1. 관리의 효율성 고려 시

      • 깊은 상속의 문제

  4. 깊은 상속의 문제

    • 상위 클래스를 바꾸면 그 아래 클래스도 모두 바뀜. 자식 클래스에서 문제가 없는지 모두 확인해야함.

    • 컴포지션도 문제는 있지만 상속보다는 덜함. 상속보다 조립성을 더 강조했기 때문.

  5. has-a, is-a 관계 : 실생활에서 개체들끼리의 관계를 기준으로 선택

    1. has-a : 컴포지션 (b has-a / b는 a를 갖고 있다. )

    2. is-a : 상속 (학생은 사람이다, 시간 강사는 선생이다. / b is -a b는 a 다. b는 a 안에 포함된다.)

  6. 다형성(polymorphism)

    1. 어떤 개체가 다양한 형태로 변할 수 있는 능력

    2. 같은 지시를 내렸는데 다른 종류의 개체가 동작을 달리하는 것.

      1. 같은 지시 : 동일한 함수 시그내처 호출

      2. 달리 동작 : 개체의 종류에 따라 실제로 실행되는 함수 구현코드가 다름(오버라이딩)

      • 절차적 언어에서 이런 일을 하려면 if문을 사용해야 함.

    3. 늦은 바인딩, 이른 바인딩

    4. 다른 종류의 개체를 편하게 저장 및 처리

    5. 그 외의 다형성 - 매개변수 다형성(제너릭), 임시 다형성(오버로딩)

    6. 다형성의 장점

      1. 캡슐화 이점 : 각 자료형의 코드가 클래스 안에 들어감.

      2. 유지보수성 높아짐

      3. 새로운 클래스를 추가할 때 클래스 코드만 추가하면 됨

      4. 클라이언트가 작성할 코드가 줄어듬.

  7. 늦은 바인딩, 이른 바인딩

    1. 늦은 바인딩(동적바인딩) : 어떤 함수 구현이 실행될지는 실행 중(런타임)에 결정됨

    2. 이른 바인딩 : 일반적인 함수 호출은 컴파일 중에 결정됨.

    • 다형성의 혜택을 받으려면 상속관계가 필요.

      • 부모개체에서 함수 시그내처 선언

      • 자식 개체에서 그 함수를 다르게 구현(오버라이딩)

    • C에서의 늦은 바인딩 : 함수포인터를 매개변수로 직접 전달.

    • 성능 : 이른 바인딩 > 늦은 바인딩 / 컴파일러가 충분한 시간을 들여 최적화를 해줌.

  8. final 키워드 : 메서드의 오버라이딩을 금지 -> 컴파일 오류

    1. C의 함수 호출과 동일하게 동작

    2. 이른 바인딩 가능

    3. final 키워드의 의미

      1. 변수 앞에 붙는 final : 변수 값 변경 불가

      2. 메서드 앞에 붙는 final : 자식 클래스에서 오버라이딩 불가

      3. 클래스 앞에 붙는 final : 상속 불가

      • final에 어긋나는 코드 작성 시 컴파일 오류

      • 베스트 프랙티스 : final을 기본적으로 붙인다.

  9. Object의 다형적 메서드

    • 자바의 클래스는 모두 Object로부터 상속을 받음

    • Object의 메서드들은 어떤 클래스에서도 오버라이딩 가능

    • 자주 쓰이는 메서드

      1. clone() - protected Object

      2. equals(Object obj) - boolean : 기본 구현은 단순 주소비교 / 오버라이딩하여 사용

      3. hashCode() -int : 어떤 개체를 대표하는 해시값을 32비트 정수로 반환

        • 동치인 두 개체는 해시 값이 같음. 동치를 바꿀 경우 hashCode()도 오버라이딩함.

        • 기본 구현체는 주소 기반 해시

        • Java가 자체 제공하는 HashMap 클래스에서 사용하기 위해 필요.

        • 해시값이 같아도 동일하지 않을 수 있음 -> 다름을 비교할 때는 빠름.

      4. toString -String : 사람이 쓰기 편하게 해당 개체를 문자열로 표현

  10. 추상 메서드, 추상 클래스

    1. 추상 메서드

      • 자식이 해당 메서드의 구현을 강제하고자 할 때 필요

      • abstract 키워드 + 함수 시그내처

      • 추상클래스에서만 선언 가능.

    2. 추상 클래스 : 인스턴스를 만들 수 없는 클래스 (반대 개념은 구체 클래스)

      • 해당 클래스의 인스턴스를 생성하지 못하게 하기 위해 -> 부모 클래스의 인스턴스를 생성하지 못하게 막기.

      • 다른 클래스의 부모 클래스가 될 수는 있음.

      • 반드시 추상 메서드가 들어있을 필요는 없음.

      • <접근 제어자> abstract class

      • 해당 클래스가 독자 생존이 불가능하면 추상클래스화.

      • 추상 클래스라도 필드 선언은 가능함, 생성자를 통한 주입, getter, setter 가능.

  11. 인터페이스

    1. 순수 추상 클래스

      • 구현, 데이터를 제외한 동작(함수 시그내처)만 모아놓은 것.

      • 주요 역할

        • 함수 포인터

        • 다형성 구현

    2. 함수 시그내처 == 함수 선언 == 인터페이스

    3. C에서 함수 포인터 매개변수는 시그내처만 지정 / OO에서 비슷한 것이 다형성

    4. 상태를 모두 제거한 클래스, 모든 메서드를 abstract로 만들어서 상속을 강요.

    5. implements 키워드 사용.

    6. 미구현 시 컴파일 오류

    7. 인터페이스는 반드시 public / 메서드도 반드시 public

      1. 인터페이스는 누구라도 보고 명령할 수 있는 동작

      2. C의 헤더 파일과 비슷하다고 보면 이해가 쉬움.

    8. 인터페이스 명명 규칙 : I + ~able

  12. 인터페이스와 다중 상속

    1. 인터페이스는 다중 상속이 가능

    2. 다중 상속은 상태와 메서드 구현이 중복되는데 인터페이스는 순수 추상클래스인 관계로 중복 상속이 가능함.

    3. 메서드 시그내처가 중복될 경우 구현체는 하나만 동작

      • ex) 인터페이스 A, B가 있고 각각 int getSomething()이라는 시그내처가 있을 경우 그 2개를 상속 받는 클래스의 getSomething() 함수가 두 인터페이스에서 호출됨.

      • 단 두 시그내처의 반환형만 다른 경우에는 컴파일 에러가 발생함. 함수 오버로딩

  13. Object의 clone() 메서드

    • 원본을 복사한 개체를 반환함. / 메모리 주소가 달라졌기 때문에 같다고 판단하지 않음.

    • 복사를 원할 경우 clone() 메서드를 구현하면 됨. 이 때 Cloneable 인터페이스를 상속 후 구현 / Object의 clone()을 오버라이딩하면 예외를 던짐.

        public final class Robot implements Cloneable {
    	     public Object clone() throws CloneNotSupportedException {
    		     return super.clone();
    	     }
        }
    
    • clone 함수 내부에서 클래스형 멤버변수가 있으면 그건 얕은 복사됨.

      • 그런 경우 해당 멤버변수도 clone 처리해줘야함.

      public final class Robot implements Cloneable {
         ... 
          public Object clone() throws CloneNotSupportedException {
              Robot cloned = (Robot) super.clone();
              cloned.head = (Head) head.clone();
      	     return cloned;
          }
       }
      
  14. Java 어노테이션

    • 프로그램에 대한 메타데이터를 제공

    • 컴파일러에게 정보를 제공

    • 컴파일 또는 배포 중에 어노테이션을 기반으로 어떤 처리를 할 수 있음.

    • 실행 중에도 어노테이션을 기반으로 어떤 처리를 할 수 있음.

    • extends로 상속 구현 시 함수의 오버라이드 여부를 컴파일 타임에 체크하기 어려운 경우가 있음

      • 자식 메서드에서 @Override를 걸어줄 경우 자식 메서드가 오버라이드된 메서드인지 체크할 수 있음. 부모 클래스에 해당 메서드 시그내처가 없을 경우 컴파일 에러

  15. 인터페이스 vs 구현

    1. 구체 클래스

      • 상태와 동작을 모두 포함

      • 동작에 다양한 접근 권한 부여

      • 개체 생성 가능

      • 다중 상속의 부모가 될 수 없음

    2. 인터페이스

      • 동작에 대한 설명만 포함

      • 모든 동작은 public

      • 이로부터 개체 생성 불가능

      • 다중 상속의 부모가 될 수 있음.

  16. 결합도

    1. 의존성 : 클래스 A가 클래스 B에 의존 - B가 없으면 A가 작동하지 못함.

    2. 결합도 : 커플링 / 모듈 간의 상호 의존성 정도

      • 여러 종류의 결합도가 존재 : 시간 결합도

    3. 결합도 판정

      • A가 B에 의존하는 상황에서 B를 변경할 때 프로그램이 잘 작동하는가?

        1. A의 내부를 변경 안 해도 제대로 동작 -> 결합도가 낮음 / 의존 정도 낮음

        2. A의 내부를 변경해야만 제대로 동작 -> 결합도가 높음 / 의존 정도 높음

    4. 결합도 낮추기

      • 의존성 주입(dependency injection) : A 클래스에서 B를 생성하지 않고 외부에서 넣어줌

        • 생성자 주입

        • setter 주입 : 개체의 생성 시 유효한 상태라는 원칙에 위배, setter를 사용 시 캡슐화에 문제가 생김.

        • 의존성 주입의 장점

          • 결합도 낮춤

          • B의 생성자가 바뀌어도 A를 바꿀 필요가 없음

        • 단점

          • 편의성

          • 프로그래머의 원래 의도를 잘 보여주는 클래스

      • 인터페이스를 이용한 추상화

    5. 상속 관계 결합도

      1. A 내부에 B 필드가 있고 B를 상속받는 C와 D가 있을 경우 C 를 전달(다형성)

  17. 디자인패턴

    1. 팩토리 메서드 패턴

      • 사용할 클래스를 정확히 몰라도 개체 생성을 가능하게 해주는 패턴

      • 클라이언트는 본인에게 익숙한 인자를 통해 개체 생성 가능

      • 생성자 오류 상황 감지 시 null 반환 가능

      • 다형적으로 개체 생성 가능.

        public enum CupSize {
        	SMALL,
        	MEDIUM,
        	LARGE
        }
        
        public final class Cup {
        	private int sizeMl;
        	
        	private Cup(int sizeMl){
        		this.sizeMl = sizeMl;
        	}
        	
        	public static Cup createOrNull(CupSize size){
        		switch (size){
        			case SMALL :
        				return new Cup(355);
        			case MEDIUM :
        				return new Cup(473);
        			case LARGE :
        				return new Cup(651);
        			default :
        				assert(false) : "Unhandled CupSize: " + size;
        				return null;
        		}
        	}
        }
        
        //사용 례
        final CupSize = ui.promptCupSizeSelection();
        order.add(Cup.createOrNull(cupSize));
        
      • 생성자 대신 정적 메서드를 사용하여 생성

        • null을 반환 가능 : 생성자는 예외를 던질 수 밖에 없음.

      • crateOrNull을 다형적으로 만들기

        public final class Cup {
        	private int sizeMl;
        	
        	Cup(int sizeMl) {
        		this.sizeMl = sizeMl;
        	}
        	
        	public int getSize() {
        		return this.sizeMl;
        	}
        }
        
        public abstract class Menu {
        	// 가상 생성자
        	public abstract Cup creatCupOrNull(CupSize size);
        }
        
        public final class AmericanMenu extends Menu {
        	@Override 
        	public Cup creatCupOrNull(CupSize size){
        		switch (size){
        			case SMALL :
        				return new Cup(473);
        			case MEDIUM :
        				return new Cup(621);
        			case LARGE :
        				return new Cup(997);
        			default :
        				assert(false) : "Unhandled CupSize: " + size;
        				return null;
        		}
        	}
        }
        
        public final class KoreanMenu extends Menu {
        	@Override 
        	public Cup creatCupOrNull(CupSize size){
        		switch (size){
        			case SMALL :
        				return new Cup(355);
        			case MEDIUM :
        				return new Cup(473);
        			case LARGE :
        				return new Cup(651);
        			default :
        				assert(false) : "Unhandled CupSize: " + size;
        				return null;
        		}
        	}
        }
        
        Menu menu = new KoreanMenu();
        Cup cup = menu.creatCupOrNull(CupSize.LARGE);
        System.out.println(cup.getSize());
        
        • 리턴형인 Cup도 다형적으로 만들기.

        public abstract class Cup {
        	private int sizeMl;
        	
        	Cup(int sizeMl) {
        		this.sizeMl = sizeMl;
        	}
        	
        	public int getSize() {
        		return this.sizeMl;
        	}
        }
        
        public class GlassCup extends Cup {
        	GlassCup(int sizeMl){
        		super(sizeMl);
        	}
        }
        
        public class PaperCup extends Cup {
        	private Lid lid;
        	
        	PaperCup(int sizeMl, Lid lid){
        		super(sizeMl);
        		this.lid = lid;
        	}
        }
        
        // 종이컵 
        public final class AmericanMenu extends Menu {
        	@Override 
        	public Cup creatCupOrNull(CupSize size){
        		Lid lid = new Lid(size);
        		switch (size){
        			case SMALL :
        				return new PaperCup(473, lid);
        			case MEDIUM :
        				return new PaperCup(621, lid);
        			case LARGE :
        				return new PaperCup(887, lid);
        			default :
        				assert(false) : "Unhandled CupSize: " + size;
        				return null;
        		}
        	}
        }
        
        Menu menu = new AmericanMenu();
        Cup cup = menu.creatCupOrNull(CupSize.LARGE);
        System.out.println(cup.getSize());
        
    2. 빌더 패턴

      • 개체의 생성 과정을 그 개체의 클래스로부터 분리하는 방법

      • 개체의 부분부분을 만들어 나가다 준비되면 그제서야 개체를 생성

      • StringBuilder 가 대표적

      	StringBuilder builder = new StringBuilder(4096);
      	builder.append(heading);
      	builder.append(newLine);
      	
      	String document = builder.toString();
      
      • 플루언트 인터페이스

        StringBuilder builder = new StringBuilder(4096);
        builder.append(heading)
        		.append(newLine);
        
        String document = builder.toString();
        
      • 그러나 호출자에서 삽입 시 실수할 가능성이 있음.

        • Java에선 매개변수 클래스를 이용해 삽입.

        • C#에선 명명된 인자로 개체를 생성함.

      • 다형적 빌더패턴

        CsvReader reader = new CsvReader(csvText);
        HtmlTableBuilder builder = new HtmlTableBuilder();
        
        reader.writeTo(builder);
        
        HtmlDocument html = builder.toHtmlDocument();
        
        CsvReader reader = new CsvReader(csvText);
        MarkdonwTableBuilder builder = new MarkdonwTableBuilder();
        
        reader.writeTo(builder);
        
        String mdText = builder.toMarkDownText();
        
        • 다형적 함수 호출 아님

    3. 랩퍼(wrapper), 어댑터 패턴

      • 어떤 클래스의 메서드 시그내처를 변경하는 패턴

      • 기존 클래스의 메서드 시그내처는 그대로 둔 상태에서 새로운 클래스를 만들어 기존 클래스를 감쌈

      • 사용 이유

        1. 추후 외부 라이브러리를 바꿀 때 클라이언트 코드를 변경하지 않기 위해

        2. 코딩 표준과 맞지 않은 경우

        3. 기존 클래스에 없는 기능을 추가하기 위해

        4. 내부 개체를 클라이언트에 노출시키지 않기 위해 (DTO)

    4. 프록시 패턴

      • 개체의 생성이 값비싼 연산을 수반할 때 그 연산을 늦추는 패턴.

      • ex) 이미지 데이터

      public final class Image {
      	private String filePath;
      	private ImageData image;
      	
      	// 개체 생성 시에는 경로 정보만 저장
      	public Image(String filePath) { 
      		this.filePath = filePath;
      	}
      	
      	// draw 호출 시 이미지를 로딩 : lazy loading(지연 로딩)
      	public void draw(Canvas canvas, float x, float y) {
      		if(this.image == null) {
      			this.image = ImageLoader.getInstance().load(this.filePath);
      		}
      	}
      	
      	canvas.draw(this.image, x ,y);			
      }
      
      • 현대의 프록시 패턴

        • 최근 컴퓨터는 메모리를 많이 장착 -> 미리 로딩해도 큰 문제 없음

        • 한 번에 그리는 이미지 수가 많이 않다면 필요할 때마다 디스크에서 읽을 수 있음

        • 그러나 인터넷 환경에선 여전히 속도에 병목이 있을 수 있으므로 여러 방법을 선택해서 사용할 것. -> 최근에는 로딩 UI를 사용하기도 함.

        • 사용자 경험(UX)를 고려하여 적절한 방법(지연로딩 or 즉시로딩) 을 선택할 것.

        public final class Image {
        private String filePath;
        private ImageData image;
        
        // 개체 생성 시에는 경로 정보만 저장
        public Image(String filePath) { 
        	this.filePath = filePath;
        }
        
        public boolean isLoaded() { 
        	return this.image != nulll; 
        }
        
        public void load() {
        	if(this.image == null) {
        		this.image = ImageLoader.getInstance().load(this.filePath);
        	}
        }
        
        public void unload() {
        	this.image = null;
        }
        
        // draw 호출 시 이미지를 로딩 : lazy loading(지연 로딩)
        public void draw(Canvas canvas, float x, float y) {
        	canvas.draw(this.image, x ,y);		
        }
        		
        

      }

      ```

    5. 책임 연쇄 패턴

      • 우선순위에 따라 여러 개체에게 일을 처리할 기회를 주려고 할 때 적합한 패턴

      public abstract class Logger {
      	private EnumSet<LogLevel> logLevels;
      	private Logger next;
      	
      	public Logger(LogLevel[] levels){
      		this.logLevels = EnumSet.copyOf(Arrays.asList(levels));
      	}
      	
      	public Logger setNext(Logger next){
      		this.next = next;
      		
      		return this.next;
      	}
      	// 잘못된 책임 연쇄 패턴 예시
      	public final void message(String msg, LogLevel severity) {
      		if(logLevels.contains(serverity)){
      			log(msg);
      		}
      		
      		if(this.next != null){
      			this.next.message(msg, severity);
      		}
      	}
      	
      	protected abstract void log(String msg);
      	
      }
      
      public class EmailLogger extends Logger {
      	public EmailLogger(LogLevel[] levels){
      		super(levels);
      	}
      	
      	@Override
      	protected void log(String msg) {
      		System.err.println("Sending via email:" + msg);
      	};
      
      }
      
      Logger logger = new ConsoleLogger(LogLevel.all());
      logger.setNext(...)
      		.setNext(...);
      
      
      • 올바른 책임 연쇄 패턴

        • 어떤 메시지를 처리할 수 있는 여러 개체가 있음

        • 이 개체들은 차례대로 메시지를 처리할 수 있는 기회를 받음

        • 만약 그중 한 개체가 메시지를 처리하면 그거에 대한 책임을 짐

          • 즉, 다음 개체는 메시지를 처리할 기회를 받지 못함

        public final void message(String msg, LogLevel severity) {
        	if(logLevels.contains(serverity)){
        		log(msg);
        	} else if(this.next != null){
        		this.next.message(msg, severity);
        	}
        }
        
    6. 옵저버 패턴

      • A를 감시하는 B 개체가 있음.

      • A에 변화가 생기면 B도 바뀌는 패턴

      • 발행-구독 (pub-sub) 패턴을 비슷하게 사용함

        • LogManger (ArrayList로 Logger 개체들을 담아두고 for 문을 돌려 처리)

      public interface IFundingCallback {
      	void onMoneyRaised(String backer, int amount);
      }
      
      public final class BookkeepingApp implements IFundingCallback {
      
      	@Override
      	public void onMoneyRaised(String backer, int amount) {
      		// 장부에 새 내역 추가
      		// amount만 사용
      	}
      }
      
      public final class MobileApp implements IFundingCallback {
      
      	@Override
      	public void onMoneyRaised(String backer, int amount) {
      		// 모바일 앱에 알림을 보여줌
      		// amount만 사용
      	}
      }
      
      public final class CrowedFundingAccount {
      	private int balance;
      	
      	private ArrayList<IFundingCallback> subscribers;
      	
      	public CrowedFundingAccount() {
      		this.subscribers = new ArrayList<IFundingCallback>();
      	}
      	
      	public void subscribe(IFundingCallback sub){
      		subscribers.add(sub);
      	}
      	
      	public void unsubscribe(IFundingCallback sub){
      		subscribers.remove(sub);
      	}
      	
      	public void support(String backer, int amount){
      		this.balance += amount;
      		
      		for(IFundingCallback sub : subscribers) {
      			sub.onMoneyRaised(backer, amount);
      		}	
      	}
      }
      
      • 옵저버 패턴은 결국 콜백 함수의 목록

      • 이런 방식은 매니지드 언어에서 메모리 누수를 만드는 주범

      // 보유한 펀딩 계좌
      CrowedFundingAccount funding;
      
      // 장부 앱을 사용 중이고 알림을 켬(구독함)
      BookkeepingApp book = new BookkeepingApp();
      funding.subscribe(book);
      
      // CrowedFundingAccount의 subscribers에서 참조를 들고 있기 때문에
      // 따라서 unsubscribe 함수를 만들어서 지워줘야함.
      funding.unsubscribe(book);
      
      // 할 일이 끝남. 장부앱 지움.
      book = null;
      
      
      
  18. 예외

    	try {
    		// 시도할 코드들
    	}catch(<예외 클래스> 변수명) {
    		// 예외 발생 시 예외처리 코드
    		// 특정하기 어렵거나 모든 예외를 잡고 싶다면 Exception 클래스 사용
    		// 다중 catch 블록 작성 시 부모 예외 클래스가 자식보다 먼저 나오면 안 됨
    	}finally {
    		// 예외 발생 여부와 상관없이 실행
    		// 보통 try 후 정상적으로 처리된 개체의 메모리를 해제하는 경우에 사용
    	}
    
    
    • 예외 발생 시 진행 순서

      1. try 블록의 실행이 중단

      2. catch 블록 중 발생한 에외를 처리할 수 있는 블록이 있는지 찾음(위에서 부터)

      3. (예외를 처리할 수 있는 catch 블록이 없다면) finally 블록 실행 후 한 단계 높은 try 블록으로 전달

      4. (예외를 처리할 수 있는 catch 블록이 있다면) catch 블록, finally 실행 후 try 이후 코드 들이 실행됨.

    • catch 블록에서 다시 예외 던지기

    	catch (<예외 클래스> e) {
    		throw e;
    	}
    
    • 커스텀 예외 클래스 - RuntimeException 이나 Exception 클래스를 상속 받아 생성

    	public final class UserNotFoundException extends RuntimeException {
    		public UserNotFoundException() {
    			super();
    		}
    		
    		public UserNotFoundException(String message) {
    			super(message);
    		}
    		
    		public UserNotFoundException(String message, Throwable cause) {
    			super(message, cause);
    		}
    	}
    
  19. checked 예외와 unchecked 예외

    1. checked 예외 - Exception을 상속 받음.

      • 컴파일러가 예외 처리를 제대로 하는지 확인 해줌

      • 어느 메서드가 어떤 예외를 던지는지 명확히 알 수 있음

      • 해당 예외들은 반드시 try-catch 블럭에서 잡아줘야함.

      • <메서드 선언> throws <예외클래스 이름> / 메서드 시그내처 옆에 던지는 예외 종류를 적어줘야함. 안하면 컴파일 오류

      • 굳이 checked 예외로 만들었다면 회복하라는 뜻

    2. unchecked 예외 - RuntimeException을 상속

      • 컴파일러가 따로 검사를 해주지 않음.

    • 상황에 맞는 예외처리 방법을 선택해야 한다.

    • 제어 흐름 용으로 예외를 사용하지 말 것. if 문 대신 사용하지 말 것.

    • 대규모 프로젝트에서 디버깅이 매우 어렵다.

  20. 다양한 오류 상황 대처법

    • 프로그램은 기본적으로 happy path를 따름

    • happy Path가 아닌 경우 오류가 발생

    • 오류 상황에 빠진 프로그램은 어떻게 진행해야 하는가?

    • 오류 상황은 예측 가능한 상황을 의미

      • 프로그램 실행 중에 기본적으로 일어나지 않는 일

      • 하지만 여전히 일어날 수 있는 일

      • 그러나 프로그래머가 예측치 못했다면

        • 버그 -> 버그 발견 후 제대로 처리하는 코드를 추가 -> 다시 빌드

    • 오류 상황을 처리하는 4가지 방법

      1. 무시하고 넘어감(무시)

        1. 바로 크래시

        2. 일단은 작동하지만 언젠가는 크래시

        3. 안정적이지 못한 상태로 계속 동작함

      2. 문제를 일으킬 수 있는 상황이 있는지 검사하고 그렇다면 프로그램을 종료한다.(종료)

        • 어떤 문제가 있었는지 사용자에게 보여주고 정상 종료

          • 팝업창, 로그파일

          • 사용자가 프로그램을 다시 실행

        • 크래시에 비해 나은 점

          • 제대로 시스템 상태를 정리하고 프로그램을 종료할 가능성이 높음

          • 프로그램 종료 후 시스템이 좀 더 안정적

        • 작업하던 내용을 날리지 않고 저장해 줄 수 있음.

      3. 문제를 일으킬 수 있는 상황이 있는지 검사하고 그렇다면 실수를 고친 뒤 계속 프로그램을 실행되게 한다(수정)

        • 입력값 검증 : 사용자에게 올바른 값을 입력하라고 다시 요청

        • 예외에 비해 성능 문제가 적음.

        • 단점

          • 문제가 처음 발생한 곳을 파악하기가 쉽지 않음

          • 문제가 발생했다는 사실조차 모른 채 오랜 시간이 흐를 수 있음

      4. 문제가 발생하면 예외를 던진다.(예외)

        1. 문제가 발생했다는 사실을 알려줄 수 있음

        2. 예외를 처리할 수 있음.

        3. 생성자에서는 반환형이 없기 때문에 예외 외에는 해결책이 없음.

          • 메모리 할당 - 메모리를 개체에 배정 - 생성자를 통해 상태 초기화

          • 생성자에서 상태 초기화 실패 시 되돌릴 방법이 없음.

      • 4가지 방법마다 적합한 상황이 있음.

      • 그러나 처음부터 오류가 없는 코드가 제일 낫다.

      • 내 시스템 안으로 들어온 데이터는 언제나 유효하다고 가정할 것

      • 남으로부터 데이터를 받아올 때 경계에서 검증하여 잘못된 데이터는 거부

        • boolean 이나 null을 반환

        • 오류코드(int 또는 enum)를 반환

        • 예외를 던짐

  21. 올바른 예외처리 방법

    1. 잘못된 예외처리가 크래시보다 더 위험하다.

    2. 크래시의 주요 문제점

      1. 작업물을 잃어버리는 경우 -> 자동 세이브로 해결

    3. 최근 OS와 하드웨어가 많이 발전하여 크래시가 나도 큰 문제는 없다.

    4. 크래시 후 메모리 덤프를 개발사 인터넷으로 전송가능함.

    5. 예외를 던질 경우

      1. 호출스택

      2. 메시지

      3. 예외 형

    6. 4가지 처리법의 순위

      1. 코드 제작자의 책임감 - 수정, 종료, 예외, 무시

      2. 코드 제작자의 오지랖 순위 - 종료, 수정, 예외, 무시

      3. 클라이언트 입장에서 객관성 순위 - 종료, 수정, 무시, 예외

      • 예외로부터 안전한 코드를 작성하려고 노력해야 함.

      • 크래시가 안나면 좀비 상태에 빠질 수도 있음.

    7. 이미 예측 된 상황

      1. 고치고 계속 프로그램 진행(수정, 예외)

      2. 예외는 두 공간 사이의 경계에서만 던질 것.

      3. 예측했는데 고치기 어려운 경우

        • 팝업창, 로그 저장

    8. 예측 못한 상황

      1. 대응 가능한 방법이 거의 없음.

      2. main에서 catch 후 로그 or 크래시

    • 무조건 실행 중에 문제를 고치려 한다고 프로그램의 안정성이 높아지는 건 아니다. 좀비 프로그램이 되는 것이 더 위험할 때가 있다.

  22. SOLID 설계 정신

    • 소프트웨어 설계를 이해하기 쉽고 유연하고 유지보수가 쉽게 만들기 위해 나온 원칙

    • 유연한 소프트웨어 설계 -> 추상적인 설계로 커플링을 제거

    • 디커플링을 강조하기 때문에 대규모 프로젝트일 경우 유용

    1. SRP : 단일책임원칙

      • 클래스의 존재 이유는 하나여야만 한다. 클래스 하나에서 너무 많은 일을 하지 말라한 함수에서 너무 많은 일을 하지 말라

      • 대부분의 사람이 이해할 수 있는 정보의 크기에 맞춰 클래스를 만들 것

      • 각 클래스의 책임을 분명하게 정의

        • 오류 상황에 대응이 용이 / 오류를 검사하고 처리해야 하는 곳이 명확함

    2. OCP : 개방-폐쇄 원칙

      • 클래스 내부 수정 없이 동작을 확장할 수 있어야 한다.

      • = 상속 + 다형성

    3. LSP : 리스코프 치환 정신

      • 부모클래스의 개체를 사용하는 코드 A가 있을 때
        나중에 자식 클래스의 개체를 거기에 대신 사용함(치환)
        이 때 a가 아무 문제 없이 작동해야함

      • 부모가 할 수 있었던 일은 자식도 다 할 수 있어야 함.

      • 그러나 100% 적용은 어려움

        • 모든 자식 개체에 대해 부모를 완벽하게 추상화하기 어려움

        • 자식을 추가할 때 부모의 동작을 바꾸는 경우가 있음

        • 예 ) 자바의 Stack 클래스. Stack은 ArrayList 등을 이용해 구현하지만 ArrayList의 기능을 전부 구현해선 안됨. -> 상속이 아니라 컴포지션으로 구현?

    4. ISP : 인터페이스 분리 정신

      • 큰 인터페이스가 몇 개 있는 것 보단 작은 인터페이스가 많이 있는 게 좋다.?

      • 인터페이스에 메서드 하나면 C의 함수 포인터와 다를 게 없다

      • 어느 정도 걸러서 들을 것. 1과 마찬가지로 대부분의 사람이 이해할 수 있는 크기에 맞춰 인터페이스를 구현할 것.

    5. DIP : 의존 역전 정신

      1. 개체끼리 통신할 때 구체적인 것 말고 추상적인 것에 의존할 것.

    • 소프트웨어 품질의 시작은 개발자의 실수를 줄이는 환경 구축

    • 모두가 이해하기 쉬운 코드를 작성할 것

    • 언제부터 디커플링을 고려해야 하는가?

      • 협업 환경에서 잦은 변경으로 서로 일을 못하는 경우

연습문제