본문 바로가기

[스프링 입문을 위한 자바 객체 지향의 원리와 이해] 요약 및 정리

by hseong 2023. 7. 16.

0.

스프링 입문을 위한 자바 객체 지향의 원리와 이해은 객체 지향 스터디를 통해서 접하게 된 서적이다. 조영호님의 오브젝트에 대한 스터디를 들어가기 전에 객체 지향에 관한 기반을 단단히 하기 위해 진행하였다.

내용 요약

1장 사람을 사랑한 기술

기계어는 0과 1밖에 모르지만 실수하는 법 없이 빠르고 정확하였다. 0과 1을 사용하는 방법조차 회사마다 달랐다.

 

어느날 기계어 명령어와 일상 용어를 일대일로 매칭하는 코드표를 만들었고 그것을 어셈블리어라 불렀다. 그러나 그 역시 기계어마다 어셈블리어가 달랐다.

 

C 언어는 여러 줄의 엄셈블리어를 단 한줄로 표현할 수 있게 되었다. 또한, 각 기계에 맞는 컴파일러로 컴파일만 하면 적절한 목적 파일이 만들어지는 One Source Multi Object Use Anywhere 를 목표로 했다. 그러나 OS의 서로 다른 특성으로 인해 기종에 맞게 소스를 변경하는 작업이 필요했다.

 

자바가 인간을 사랑한 방식은 가상 머신이다. 단 하나의 컴파일러만으로 어디서든 실행시킬 수 있는 목적 파일을 만들 수 있게 된것이다.

2장 자바와 절차적 구조적 프로그래밍

해당 챕터에서는 자바의 메모리 구조를 T 메모리라 지칭하며 static, stack, heap 영역에 대해서 설명한다.

 

static 영역은 클래스가 배치되는 영역이며 해당 클래스가 실제로 사용될 때 클래스 로더에 의해서 동적으로 로딩된다.

 

stack 영역은 함수 호출시 해당 함수의 스택 프레임이 생성되어 순차적으로 쌓이게 된다.

 

heap 영역은 클래스의 인스턴스가 생성되어 머무는 영역이다. 해당 인스턴스에 대한 참조가 사라지면 GC와 함께 소멸된다.

3장 자바와 객체 지향

결국 객체 지향은 인간의 눈높이에서 개발하기 위해 탄생하였다.

 

세상 모든 것들이 객체들의 합이며 이처럼 현실을 인지하는 방식으로 개발하기에 객체 지향은 직관적이다.

 

1) 추상화

추상화란 구체적인 것을 분해해서 관찰자가 관심 있는 특성만 가지고 재조합하는 것이다.

 

세상에 존재하는 유일무이한 객체를 특성(속성 + 기능)에 따라 분류하면 집합적 개념인 클래스가 나온다.

  • 사람이라는 클래스를 위해 사람 객체들을 관찰해서 이들이 가진 공통된 특성을 찾는다.
  • 시력, 몸무게, 나이, 직업 등 명사로 표현되는 특성을 속성이라 하며 특정한 값을 가진다.
  • 먹다, 자다, 일하다 등 동사로 표현되는 특성을 행위라 하여 객체 지향에서는 이를 메서드라 한다.

같은 사람이라는 클래스더라도 문맥에 따라 서로 다른 특성을 가질 수 있다. 병원이라는 문맥에서 사람은 환자일 수 있고, 은행이라는 문맥에서 사람은 고객일 수 있다. 애플리케이션의 경계에 따라 필요한, 필요없는 특성들이 있다.

 

책에서는 이를 이렇게 표현한다.

추상화란 구체적인 것을 분해해서 관심 영역(애플리케이션 경계, Application Boundary)에 있는 특성만 가지고 재조합하는 것이다. (=모델링)

2) 상속

객체 지향에서 상속은 재사용과 확장을 의미한다.

 

상위 클래스 쪽으로 갈수록 추상화, 일반화됐다고 하며, 하위 클래스 쪽으로 갈수록 구체화되었다고 말한다. 객체 지향의 상속은 부모 - 자식 같은 계층도보다는 동물 - 포유류 같은 분류도라 이해해야 한다.

자식은 부모다. (X)
포유류는 동물이다. (O)

책에서는 상속에 대해 is a kind of 관계로 표현한다.

 

상속은 하위 클래스가 상위 클래스의 특성을 확장한다. 그리고 인터페이스의 경우 be able to의 관계로 클래스가 무엇을 할 수 있다라고 하는 기능을 구현하도록 강제한다.

3) 다형성

기본적으로 다형성은 오버라이딩과 오버로딩이라고 할 수 있다.

 

상위 클래스 타입의 참조 변수를 사용하더라도 하위 클래스에서 오버라이딩한 메서드가 호출된다.

 

오버라이딩을 통한 메서드 재정의, 오버로딩을 통한 메서드 중복 정의를 통한 다형성을 이용하여 프로그램 작성시 사용자 편의성을 제공한다.

4) 캡슐화

접근 제한자를 통해 객체의 상태를 외부로 노출되지 않게 숨긴다. 각각의 접근 제한자의 범위가 어디까지인지 확실하게 이해하고 있어야 한다.

public - 모든 곳에서 접근 가능

protected - 자식 클래스/동일한 패키지에서 접근 가능

[default] - 동일한 패키지에서 접근 가능

private - 본인만 접근 가능

참조 변수의 복사

Call By ValueCall By Reference는 본질적으로 차이가 없다.

  • 기본 자료형 변수는 저장하고 있는 값을 그 값 자체로 해석한다.
  • 객체 참조 변수는 저장하고 있는 값을 주소로 해석한다.
  • 변수를 복사하든 참조 변수를 복사하든 결국 변수가 가진 값이 그대로 복사된다.

4장 자바가 확장한 객체 지향

본 장에서는 자바가 객체 지향을 실현하기 위한 다양한 키워드에 대해서 설명하고 있다.

 

abstract는 자식 클래스에 반드시 존재해야 하는 메서드의 구현을 강제하기 위해 사용할 수 있다.

 

생성자 또한 메서드이다. 반환값이 없고 클래스명과 같은 이름을 가진 메서드를 객체를 생성하는 메서드라 하여 객체 생성자 메서드, 생성자라 한다.

 

static 블록은 클래스가 static 영역에 배치될 때 실행되는 코드 블록이다. 해당 블록의 내용은 단 한 번만 실행되며 클래스가 로딩되는 시점은 다음과 같다.

  • 클래스의 정적 속성을 사용할 때
  • 클래스의 정적 메서드를 호출할 때
  • 클래스의 인스턴스를 최초로 만들 때

final은 클래스에 사용한다면 상속을 허락하지 않겠다는 의미이다. 변수에 사용한다면 변경 불가능한 상수가 된다. 메서드에 사용한다면 오버라이딩이 불가능한 메서드가 된다.

 

instanceof는 객체가 특정 클래스의 인스턴스인지 물어보는 연산자이다. 참조 변수의 타입이 아니라 실제 객체의 타입에 의해 처리한다. 단, LSP(리스코프 치환 원칙)을 어기는 코드에서 주로 나타나는 연산자로 냄새 나는 코드가 아닌지 점검이 필요하다.

 

package는 네임스페이스를 만들어주는 역할을 한다. 동일한 클래스 이름을 가지더라도 패키지를 통해 서로를 구분할 수 있다.

 

interface는 추상 메서드만을 가질 수 있다. 자바 8 이후로 정적 상수, default 메서드를 가질 수 있게 되었다.

 

this는 자기 자신에 대한 참조이다. 지역 변수와 인스턴스 변수의 이름이 같은 경우 지역 변수가 우선하며 이때 this.변수명으로 인스턴스 변수에 접근가능하다. 클래스 변수는 클래스명.변수명을 통해 접근할 수 있다. this.변수명으로도 접근은 가능하나 올바른 방식은 아니다.

 

super는 상위 클래스의 인스턴스를 지칭하는 키워드이다. 단, super.super 형태로 상위, 상위의 클래스 인스턴스에는 접근이 불가하다.

 

객체 멤버 메서든느 객체별로 달라지지 않는다. 메서드 안에서 사용하는 멤버 속성의 값만 다를 뿐이다. JVM은 객체 멤버 메서드를 static 영역에 단 하나만 보유한다. 그리고 메서드 호출시에 this를 인수로 넘긴다.

5장 SOLID

SRP - 단일 책임 원칙

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.

 

하나의 클래스는 하나의 역할과 책임을 가져야 한다. 메서드가 여러 책임을 가지고 있을 때 나타나는 대표적인 냄새가 분기처리를 위한 if 문이다.

 

애플리케이션의 경계를 정의하고 추상화를 통해 클래스를 선변하고 속성과 메서드를 설계할 때 SRP를 고려하는 습관을 가져야 한다.

OCP - 개방 폐쇄 원칙

확장에서는 열려 있어야 하고, 주변의 변경에는 닫혀 있어야 한다.

 

스프링을 예로 들어보자.

 

스프링은 DataSource를 이용하여 데이터베이스 커넥션을 얻어오는 방식을 추상화하였다. 개발자는 커넥션을 획득하는 구체적인 방식에 대해 전혀 알 필요 없음은 물론, 새로운 방법이 추가되더라도 전혀 코드 변경을 수행할 필요가 없다.

LSP - 리스코프 치환 원칙

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.

 

기억하자. 상속에 있어서 슈퍼 타입 - 서브 타입을 표현하는 형태는 계층도와 같이 동물 - 포유류 - 고래와 같은 관계이다.

ISP - 인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.

 

단일 책임 원칙과 인터페이스 분할 원칙은 같은 문제에 대한 두 가지 해결책이다.

 

다양한 행위를 수행하는 만능 클래스를 바라보는 인터페이스에 따라 수행 가능한 행위를 제한시킬 수 있다. 일반적으로 단일 책임 원칙에 따라 클래스가 하나의 역할만 수행하도록 분리하는 것이 더 좋은 해결책이다.

DIP - 의존 역전 원칙

고차원 모듈이 저차원 모듈에 의존해서는 안 된다. 추상화된 것은 구체적인 것에 의존해선 안 되고, 구체적인 것이 추상화된 것에 의존해야 한다.

 

저차원 모듈일 수록 변경되지 쉽다. 추상화된 인터페이스나 상위 클래스에 의존하여 변경에 대한 영향을 최소화하는 것이 의존 역전 원칙이다.

6장 스프링이 사랑한 디자인 패턴

스프링에는 다양한 디자인 패턴이 녹아있다. 저자는 스프링에 대해서 OOP 프레임워크라고 설명하며 다음의 디자인 패턴에 대해서 소개하고 있다.

  • 어댑터 패턴
  • 프록시 패턴
  • 데코레이터 패턴
  • 싱글턴 패턴
  • 템플릿 메서드 패턴
  • 팩토리 메서드 패턴
  • 전략 패턴
  • 템플릿 콜백 패턴

템플릿 콜백 패턴의 경우 전략 패턴의 변형으로 스프링의 DI에서 사용하는 특별한 형태의 전략 패턴이다.

7장 스프링 삼각형과 설정 정보

저자는 의존성에 대해 다음과 같이 단순한 형태로 정의하였다.

 

의존성은 new다.

다음과 같은 코드가 있다고 해보자.

public class Robot {
    private Arm arm;

    public Robot() {
        arm = new TitanumArm();
    }
}

Robot에서 TitanumArm의 방향으로 의존이 발생한다. (Robot -> TitanumArm) 더 큰 Robot이 일부분인 TitanumArm에 의존하게 되는 것이다.

 

만일 로봇이 다른 기계팔을 사용해야 한다면 그 때마다 내부 코드가 변경되어야 한다.

 

이를 해결하기 위한 대표적인 방법이 외부에서 의존성을 주입받는 것이다.

public Robot(Arm arm) {
    this.arm = arm;
        }

이로써 새로운 Arm이 추가되더라도 Robot의 코드는 전혀 변경할 필요가 없다. 재컴파일, 재배포 또한 하지않아도 된다.

스프링의 의존성 주입

외부에서 의존성을 주입해주기 위해서는 결국 어디선가는 TitanumArm을 생성해주어야 한다.

 

스프링을 도입하게 되면 더 이상 그 어딘가(예를 들어 RobotService)는 객체를 생성할 책임을 지지 않는다. 만일 Arm을 다른 걸로 바꾸고 싶다면 설정 정보만 약간 손보면 된다.

AOP

입금, 출금과 같이 여러 모듈에서 로깅, 트랜잭션과 같이 핵심 비즈니스 로직이 아닌 부가적인 목적으로 필요한 로직이 반복적으로 나타나는 것을 횡단 관심사(cross-cutting concern) 라 한다.

 

이러한 핵심 로직과 부가 로직을 분리하고 선택적으로 적용하기 위한 모듈이 애스펙트(Aspect)이다. 애플리케이션을 바라보는 관점을 횡단 관심사 관점에서 바라보고 이를 이용한 프로그래밍 방식을 관점 지향 프로그래밍, AOP라 한다.

AOP는 OOP와 별개의 개념이 아닌 코드를 좀 더 객체 지향적으로 작성할 수 있도록 도움을 준다.

 

AOP를 적용한다면 다음과 같은 트랜잭션 로직과 비즈니스 로직이 섞인 코드를

public void transfer() {
    Connection con = dataSource.getConnection();
    try {
        con.setAutoCommit(false);

        //비즈니스 로직

        con.commit();
    } catch (Exception e) {
        con.rollback();
        throw new IllegalStateException(e);
    } finally {
        release(con)
    }
}

다음과 같이 별개의 로직으로, 하나의 책임만을 가지도록 분리할 수 있다.

public void doTransaction(ProceedingJoinPoint joinPoint) {
    Connection con = dataSource.getConnection();
    try {
        con.setAutoCommit(false);

        joinPoint.proceed();

        con.commit();
    } catch (Exception e) {
        con.rollback();
        throw new IllegalStateException(e);
    } finally {
        release(con)
    }
}
public void transfer() {
    //비즈니스 로직
}

스프링 AOP

스프링 AOP를 구현하는 방법은 인터페이스를 이용하는 JDK Proxy와 상속을 이용하는 CGLIB 두 가지 방법이 존재한다.

 

책은 XML을 이용하여 AOP를 등록하고 있으며 인터페이스 기반의 JDK Proxy를 이용하여 스프링 AOP를 사용하고 있다. 책과는 상관 없는 내용이지만 짚고 넘어가고 싶은 부분이 하나 있다.

 

다음은 스프링 부트가 어째서 CGLIB를 기본 프록시 생성 전략으로 사용하는가에 대한 정리이다. 해당 내용은 김영한님의 스프링 고급편 AOP 파트를 참고하여 작성했다.

 

저자는 인터페이스, JDK Proxy를 이용해서 스프링 AOP를 구현하고 있다. 이에 대해 책에서 이르길,

인터페이스 없이 CGLIB 라이브러리를 이용하는 방법도 있긴 하지만 귀찮은 설정 작업을 많이 해야 한다.

라고 표현하였다.

그러나 책에서 설명하는 AOP에 관한 내용은 스프링 3.0기준이다. 저자의 블로그에 관련된 코드, 게시글이 업로드 된 것은 2013년 경이며 당시의 스프링 AOP와 현재의 스프링 AOP는 많은 변경점이 있다.

  • 스프링은 3.2부터 CGLib를 스프링 내부에 함께 패키징하였다. 더 이상 번거롭게 사용자가 직접 라이브러리를 추가해주지 않아도 된다.
  • 기존에 CGLib를 이용해 만든 프록시는 생성자를 2번 호출하는 문제가 있었다. 스프링 4.0부터 특별한 라이브러리를 이용하여 해결하였다.
  • 스프링 부트 2.0 부터 프록시 생성에 CGLib를 기본으로 사용하도록 변경되었다. 인터페이스가 있어도 CGLib를 사용해서 구체클래스를 기반으로 프록시를 생성한다.

어째서 CGLib Proxy가 default가 되었는가?

스프링 부트 프로젝트의 리더는 CGLIB를 기본으로 사용하는 이유에 대해서 다음과 같이 설명한다.

JDK Proxy는 인터페이스를 기반으로 프록시를 생성한다.

 

프록시를 인터페이스로 캐스팅할 순 있지만, 구체 클래스로의 캐스팅은 불가능하다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8#
  • MemberServiceProxyMemberServiceImpl에 대해서 전혀 알지 못한다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8#

반면, CGLib는 구체 클래스를 기반으로 프록시를 생성한다.

 

따라서 구체 클래스 혹은 그보다 상위의 인터페이로의 자유로운 타입 캐스팅이 가능하다.

 

문제는 의존관계 주입에 있다.

JDK Proxy를 사용했을 때의 문제는 의존관계 주입시 발생한다.

private  MemberService memberService;
private MemberServiceImpl memberServiceImpl;
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8#
  • MemberServiceMemberServiceProxy는 정상적으로 주입될 것이다.
  • MemberServiceImplMemberServiceProxy는 주입이 불가능할 것이다. 프록시는 다른 구체 클래스에 대해 전혀 알지 못한다.

'' 카테고리의 다른 글

[TDD 시작하기] 후기 및 정리  (0) 2023.06.04
[클린 코드] 요약 및 정리  (1) 2023.05.19