코드 스테이츠

Spring Framework의 특징

한휘용 2023. 5. 31. 14:57
728x90

Spring Framework의 특징

 

  • POJO(Plain Old Java Object)

Spring 삼각형 - POJO

위 그림은 Spring 삼각형이라는 그림이다.

이 Spring 삼각형 하나로 Spring의 핵심 개념들을 모두 표현하고 있다고 해도 과언이 아니다.

 

위 그림에서 POJO는 Spring에서 사용하는 핵심 개념들에 둘러싸여 있는 모습이다. 이는 POJO라는 것을 IoC/DI, AOP, PSA를 통해서 달성할 수 있다는 것을 의미한다. Spring 삼각형에서 가운데에 있는 POJO라는 개념을 먼저 알아보자.

 

 

 

POJO(Plain Old Java Object)란?

 

POJO는 Plain Old Java Object라는 단어의 첫 글자를 따서 만든 약자이다.

 

Java로 짜인 코드는 어떤 식으로든 객체와 객체가 관계를 맺을 수 밖에 없는 객체지향 프로그래밍이기 때문에 POJO중 'JO(Java Object)'는 객체지향 프로그래밍을 의미하는 부분이라고 말할 수 있다.

 

그렇다면 'PO(Plain Old)'는 무엇을 의미할까?

'Plain'이라는 의미는 일상생활에서 요거트를 주문할 때 자주 사용하는 용어다. 플레인 요거트는 아무것도 추가되지 않은 순수한 요거트 자체만을 말한다.

이처럼 POJO에서 ‘PO’는 Java로 생성하는 순수한 객체를 의미한다. 

POJO가 평범한 Java 객체를 의미하는 게 맞지만 프로그래밍 관점에서 조금 더 깊은 의미가 있다.

 

 

POJO 프로그래밍이란?

 

POJO 프로그래밍이란 POJO를 이용해서 프로그래밍 코드를 작성하는 것을 의미한다. 

그런데 단순히 순수 자바 객체만을 사용해서 프로그래밍 코드를 작성한다고 해서 POJO 프로그래밍이라고 볼 수는 없다.

 

POJO 프로그래밍으로 작성한 코드라고 불리기 위해서는 크게 두 가지 정도의 기본적인 규칙은 지켜주어야 한다.

 

1. java나 Java의 스펙(사양)에 정의된 것 이외에는 다른 기술이나 규약에 얽매이지 않아야 한다.

 

예시 코드를 통해 첫 번째 규칙의 의미를 확인해보자.

getter, setter만 가지고 있는 예시코드

public class User {
  private String userName;
  private String id;
  private String password;

  public String getUserName() {
    return userName;
  }

  public void setUserName(String userName) {
  	this.userName = userName;
  }

  public String getId() {
  	return id;
  }

  public void setId(String id) {
  	this.id = id;
  }

  public String getPassword() {
  	return password;
  }

  public void setPassword(String password) {
  	this.password = password;
  }
}

위 예시 코드는 자바에서 제공하는 기능만 사용하여 getter, setter만 가지고 있는 코드이다.

해당 클래스의 코드에서는 Java언어 이외에 특정한 기술에 종속되어 있지 않은 순수 객체이기 때문에 POJO라고 부를 수 있다.

 

특정 기술에 종속적인 예시코드

public class MessageForm extends ActionForm{ // (1)
	
	String message;

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}
	
}

public class MessageAction extends Action{ // (2)
	
	public ActionForward execute(ActionMapping mapping, ActionForm form,
		HttpServletRequest request, HttpServletResponse response)
        throws Exception {
		
		MessageForm messageForm = (MessageForm) form;
		messageForm .setMessage("Hello World");
		
		return mapping.findForward("success");
	}
	
}

위 예시 코드는 Java 코드가 특정 기술에 종속적인 예를 보여주기 위한 코드이다.

ActionForm 클래스는 과거에 Struts라는 웹 프레임워크에서 지원하는 클래스다.

(1)에서는 Struts라는 기술을 사용하기 위해서 ActionForm을 상속하고 있다.

(2)에서는 역시 Struts 기술의 Action 클래스를 상속받고 있다.

 

이렇게 특정 기술을 상속해서 코드를 작성하게 되면 나중에 애플리케이션의 요구사항이 변경돼서 다른 기술로 변경하려면 Strust의 클래스를 명시적으로 사용했던 부분을 전부 다 일일이 제거하거나 수정해야 한다.

 

그리고, Java는 다중 상속을 지원하지 않기 때문에 'extends' 키워드를 사용해서 한 번 상속을 하게 되면 상속클래스를 상속받아서 하위 클래스를 확장하는 객체지향 설계 기법을 적용하기 어려워지게 된다.

 

 

2. 특정 환경에 종속적이지 않아야 한다.

 

서블릿(Servlet) 기반의 웹 애플리케이션을 실행시키는 서블릿 컨테이너(Servlet Container)인 아파치 톰캣(Apache Tomcat)을 예로 들어보자.

 

순수 Java로 작성한 애플리케이션 코드 내에서 Tomcat이 지원하는 API를 직접 가져다가 사용한다고 가정 한다.

 

그런데 만약에 시스템의 요구 사항이 변경되어서 Tomcat 말고 제티(Zetty)라는 다른 Servlet Container를 사용하게 된다면 어떻게 될까?

 

애플리케이션 코드에서 사용하고 있는 Tomcat API 코드들을 모두 걷어내고 Zetty로 수정해야한다. 최악의 경우에는 애플리케이션을 전부 뜯어고쳐야 될지도 모르는 상황에 직면하게 될 수 있다.

그렇기 때문에 특정 환경에 종속적이지 않아야 한다.

 

 

POJO 프로그래밍이 필요한 이유

 

POJO 프로그래밍이 필요한 이유는 앞에서 설명한 예시에 잘 나와 있다.

  • 특정 환경이나 기술에 종속적이지 않으면 재사용 가능하고, 확장 가능한 유연한 코드를 작성할 수 있다.
  • 저수준 레벨의 기술과 환경에 종속적인 코드를 애플리케이션 코드에서 제거함으로써 코드가 깔끔해진다.
  • 코드가 깔끔해지기 때문에 디버깅하기도 상대적으로 쉽다.
  • 특정 기술이나 환경에 종속적이지 않기 때문에 테스트 역시 단순해진다.
  • 객체지향적인 설계를 제한 없이 적용할 수 있다.(가장 중요한 이유)

이처럼 POJO 프로그래밍이 필요한 이유를 생각하다 보면 자연스럽게 객체지향적인 사고에 대한 고민을 하게 되고, 이러한 사고를 통해서 Spring을 조금 더 객체지향적으로 사용할 수 있을 거라고 생각한다.

 

 

POJO와 Spring의 관계

 

Spring은 POJO 프로그래밍을 지향하는 Framework 이다.

Spring Framework를 사용하기 전에는 원하는 특정한 기술이 있다면 해당 기술을 직접적으로 사용하는 객체를 만들어 사용했다. 다만 프로젝트가 커지고 필요한 기술들이 늘어나며 특정 기술과 환경에 종속되는 경우가 자주 발생하여 작성된 코드의 유지/보수가 어렵고, Java에서의 상속(extends)의 특성상 이미 특정 클래스를 상속하게 되어 정작 다른 상위 클래스를 상속해서 기능을 확장하기 어려운 경우도 많이 발생했다. 결국 좋은 객체지향 설계를 할 수 있는 Java 언어를 사용하면서도 객체지향 설계 본질을 잃어버리는 문제점들을 해결하고자 POJO라는 개념이 등장하게 되었다.

그리고 최대한 다른 환경이나 기술에 종속적이지 않도록 하기 위한 POJO 프로그래밍 코드를 작성하기 위해서 Spring에서는 세 가지 기술을 지원하고 있다.

 

그 세 가지 기술은 바로 Spring 삼각형에서 POJO를 감싸고 있는 IoC/DI, AOP, PSA다.

 

  • IoC(Inversion of Control)
 

IoC(Inversion of Control)란?

애플리케이션 흐름의 주도권이 뒤바뀐 것, 즉 애플리케이션 흐름의 주도권을 Spring이 갖는다 라는 것이다.

 

Java 콘솔 애플리케이션의 일반적인 제어권

순수 Java 코드만으로 간단한 메세지를 콘솔에 출력하는 프로그램 코드 예시

public class Example2_10 {
    public static void main(String[] args) {
        System.out.println("Hello IoC!");
    }
}
 
일반적으로 위 코드 예시와 같은 Java 콘솔 애플리케이션을 실행하려면 main() 메서드가 있어야 한다.
그래야 main() 메서드 안에서 다른 객체의 메서드를 호출한다던가 하는 프로세스가 진행이 되니까. 
 
코드 예시에서는 main 메서드가 호출된 후에 System 클래스를 통해서 static 멤버 변수인 out이 println()을 호출한다.
 
이렇게 개발자가 작성한 코드를 순차적으로 실행하는게 애플리케이션의 일반적인 제어흐름이다.
 
 

Java 웹 애플리케이션에서 IoC가 적용되는 예

Java 콘솔 애플리케이션이 아니라 웹 상에서 돌아가는 Java 웹 애플리케이션의 경우를 생각해보자.

서블릿 컨테이너의 서블릿 호출 예

[그림 2-3]은 서블릿 기반의 애플리케이션을 웹에서 실행하기 위한 서블릿 컨테이너의 모습이다.

Java 콘솔 애플리케이션의 경우 main() 메서드가 종료되면 애플리케이션의 실행이 종료된다.

 

하지만 웹에서 동작하는 애플리케이션의 경우 클라이언트가 외부에서 접속해서 사용하는 서비스이기 때문에 main() 메서드가 종료되지 않아야 한다.

 

그런데 서블릿 컨테이너에는 서블릿 사양(Specification)에 맞게 작성된 서블릿 클래스만 존재하지 별도의 main() 메서드가 존재하지 않는다.

 

main() 메서드가 없는데 어떻게 애플리케이션이 실행되는 걸까?

 

서블릿 컨테이너의 경우, 클라이언트의 요청이 들어올 때마다 서블릿 컨테이너 내의 컨테이너 로직(service() 메서드)이 서블릿을 직접 실행시켜 주기 때문에 main() 메서드가 필요 없습니다.

 

이 경우에는 서블릿 컨테이너가 서블릿을 제어하고 있기 때문에 애플리케이션의 주도권은 서블릿 컨테이너에 있다.

바로 서블릿과 웹 애플리케이션 간에 IoC(제어의 역전)의 개념이 적용되어 있는 것이다.

 

그렇다면 Spring에는 이 IoC의 개념이 어떻게 적용되어 있을까?

답은 바로 DI(Dependency Injection)다.

 

 

  • DI(Dependency Injection)

Dependency는 ‘의존하는 또는 종속되는’이라는 의미를, Injection은 ‘주입’이라는 의미를 가지고 있다.

두 단어를 합쳐서 의미를 파악해 보면 DI는 '의존성 주입' 을 의미하게 된다.

 

즉, DI(Dependency Injection) 의존성 주입이라는 의미로 IoC 개념을 조금 구체화시킨 것이라고 볼 수 있다.

 

그렇다면 의존성 주입은 대체 무엇일까?

객체지향 프로그래밍에서 의존성이라고 하면 대부분 객체 간의 의존성을 의미한다.

 

A, B라는 두 개의 클래스 파일을 만들어서 A 클래스에서 B클래스의 기능을 사용하기 위해 B 클래스에 구현되어 있는 어떤 메서드를 호출하는 상황을 생각해보자.

클래스 간의 의존 관계 예시

위 그림은 A 클래스가 B 클래스의 기능을 사용하는 것을 클래스 다이어그램으로 표현한 것이다.

이처럼 A 클래스가 B 클래스의 기능을 사용할 때, ‘A클래스는 B클래스에 의존한다’라고 한다.

 

‘A 클래스의 프로그래밍 로직 완성을 위해 B 클래스에게 도움을 요청한다. ‘ 즉, ‘B 클래스에게 의지(의존)한다’라고 생각하면 좋을 것 같다.

 

코드를 통해 의존성 주입을 좀더 구체적으로 알아보자.

 

클래스  간의 의존 관계 성립 예시코드

클래스 간의 의존 관계 예시

예시 코드에서 MenuController클래스는 클라이언트의 요청을 받는 엔드포인트(Endpoint) 역할을 하고, MenuService클래스는 MenuController클래스가 전달받은 클라이언트의 요청을 처리하는 역할을 한다.

클라이언트 측면에서 서버의 엔드포인트(Endpoint)란 클라이언트가 서버의 자원(리소스, Resource)을 이용하기 위한 끝 지점을 의미한다.

MenuController 클래스는 메뉴판에 표시되는 메뉴 목록을 조회하기 위해서 MenuService의 기능을 사용하고 있다.

 

Java의 객체 생성 방법인 new 키워드를 사용해서 MenuService 클래스의 객체를 생성한 후, 이 객체로 MenuService의 getMenuList() 메서드를 호출하고 있다.

 

이처럼 클래스끼리는 사용하고자 하는 클래스의 객체를 생성해서 참조하게 되면 의존 관계가 성립하게 된다.

 

이렇게 두 클래스 간에 의존 관계는 성립되었지만 아직까지 의존성 주입은 이루어지지 않았다.

 

이제 의존성 주입을 해보도록 하자.

의존성 주입 예시

위 예시는 의존성 주입이 일어나는 예시다.

 

이전 클래스간의 의존 관계 예시에서는 MenuService의 기능을 사용하기 위해 MenuController에서 MenuService의 객체를 new 키워드로 직접 생성한 반면 의존성 주입 예시에서는 MenuController 생성자로 MenuService의 객체를 전달받고 있다.

 

이처럼 생성자를 통해서 어떤 클래스의 객체를 전달받는 것을 ‘의존성 주입’이라고 한다.

생성자의 파라미터로 객체를 전달하는 것을 외부에서 객체를 주입한다라고 표현을 하는 것이다.

 

그렇다면 여기서 의미하는 객체를 주입해 주는 ‘외부’는 무엇일까?

바로 CafeClient 클래스가 MenuController의 생성자 파라미터로 menuService를 전달하고 있기 때문에 객체를 주입해 주는 외부가 된다. 의존성 주입이란 이게 전부다.

 

의존성 주입은 느슨한 결합인 클래스들 간의 강한 결합을 피하기 위한 것

 

느슨한 결합 : 어떤 클래스가 인터페이스 같이 일반화된 구성 요소에 의존하고 있는 경우를 말한다.

 

애플리케이션 코드 내부에서 직접적으로 new 키워드를 사용할 경우, SOLID의 DIP인 의존 관계 역전 원칙에 위반된다.

왜냐하면 new로써 생성한 구체화된 클래스에 의존하고 있는 형식이 되기 떄문이다. 따라서  좋은 객체지향 설계가 아니라서 이것을 피하기 위해 DI가 적용되었다라고 생각해도된다.

 

  • AOP(Aspect Oriented Programming)

AOP(Aspect Oriented Programming)란?

한글로 번역하면 관심 지향 프로그래밍 정도로 해석할 수 있다.

 

OOP(Object Oriented Programmig)란 객체 지향 프로그래밍 즉, 객체 간의 관계를 지향하는 프로그래밍 방식을 의미한다

 

그렇다면 관심(Aspect)을 지향하는 프로그래밍에서 관심은 무엇을 의미하는 것일까?

 

AOP에서 Aspect는 애플리케이션에 필요한 기능 중에서 공통적으로 적용되는 공통 기능에 대한 관심과 관련이 있다.

 

 

공통 관심 사항과 핵심 관심 사항

 

애플리케이션을 개발하다 보면 애플리케이션 전반에 걸쳐 공통적으로 사용되는 기능들이 있기 마련인데, 이러한 공통 기능들에 대한 관심사를 공통 관심 사항(Cross-cutting concern)이라고 한다.

 

그리고 흔히들 말하는 비즈니스 로직 즉, 애플리케이션의 주목적을 달성하기 위한 핵심 로직에 대한 관심사를 핵심 관심 사항(Core concern)이라고 한다.

 

AOP는 '핵심 관심사항만 있는 코드들 전부에 각 각 공통 관심사항을 넣기에 매우 번거롭기에, 공통 관심사항과 핵심 관심사항을 분리하기 위해 사용하는 것' 이다.

조금더 구체적으로 말한다면 애플리케이션의 핵심 업무 로직에서 로깅이나 보안, 트랜잭션 같은 공통 기능 로직들을 분리하는 것이라고 말할 수 있다.

 

 

  • PSA(Portable Service Abstraction)

 

PSA(Portable Service Abstraction)란?

 

객체지향 프로그래밍 세계에서는 어떤 클래스의 본질적인 특성만을 추출해서 일반화하는 것을 바로 추상화(Abstraction)라고 한다.

 

객체지향 프로그래밍 언어인 Java에서 코드로 추상화를 표현할 수 있는 대표적인 방법이 바로 추상 클래스와 인터페이스이다.

 

예를 들어, 미취학 아동을 관리하는 애플리케이션을 설계하면서 아이 클래스를 일반화(추상화) 한다라고 가정해보자.

 

미취학 아동을 관리하기 위해 필요한 아이의 특징에는 뭐가 있는지 알아야 한다.

Java의 클래스는 속성을 나타내는 멤버 변수와 동작을 나타내는 메서드로 구성되므로, 아기의 속성과 동작을 일반화 해서 멤버 변수와 메서드로 표현해 보도록 하겠다.

 

아이를 관리하는 관점에서 아이의 일반적인 속성으로 이름, 키, 몸무게, 혈액형, 나이 등이 있다.

그리고 일반적으로 아이가 할 수 있는 동작으로는 웃다, 울다, 자다, 먹다 등을 설정한다.

 

위 조건을 바탕으로 일반적인 아기의 특징을 클래스로 작성해보자.

public abstract class Child {
    protected String childType;
    protected double height;
    protected double weight;
    protected String bloodType;
    protected int age;

    protected abstract void smile();
    protected abstract void cry();
    protected abstract void sleep();
    protected abstract void eat();
}

 

위 코드를 확장하는 코드 예시를 통해 추상화를 적용해보자

// NewBornBaby.java(신생아)
public class NewBornBaby extends Child {
    @Override
    protected void smile() {
        System.out.println("신생아는 가끔 웃어요");
    }

    @Override
    protected void cry() {
        System.out.println("신생아는 자주 울어요");
    }

    @Override
    protected void sleep() {
        System.out.println("신생아는 거의 하루 종일 자요");
    }

    @Override
    protected void eat() {
        System.out.println("신생아는 분유만 먹어요");
    }
}

// Infant.java(2개월 ~ 1살)
public class Infant extends Child {
    @Override
    protected void smile() {
        System.out.println("영아는 많이 웃어요");
    }

    @Override
    protected void cry() {
        System.out.println("영아는 종종 울어요");
    }

    @Override
    protected void sleep() {
        System.out.println("영아부터는 밤에 잠을 자기 시작해요");
    }

    @Override
    protected void eat() {
        System.out.println("영아부터는 이유식을 시작해요");
    }
}

// Toddler.java(1살 ~ 4살)
public class Toddler extends Child {
    @Override
    protected void smile() {
        System.out.println("유아는 웃길 때 웃어요");
    }

    @Override
    protected void cry() {
        System.out.println("유아는 화가나면 울어요");
    }

    @Override
    protected void sleep() {
        System.out.println("유아는 낮잠을 건너뛰고 밤잠만 자요");
    }

    @Override
    protected void eat() {
        System.out.println("유아는 딱딱한 걸 먹기 시작해요");
    }
}

위 코드 예시는 Child클래스를 확장한 하위 클래스들의 예시다.

 

추상 클래스를 통해서 아이의 일반적인 특징을 Child 클래스로 작성했다면 Child클래스를 확장한 하위 클래스들은 Child 클래스의 일반화된 동작을 자신만의 고유한 동작으로 재구성하고 있다.

 

그렇다면 해당 연령에 맞는 NewBornBaby, Infant, Toddler 클래스를 사용하기 위해서 어떤 방식으로 접근하면 될까?

public class ChildManageApplication {
    public static void main(String[] args) {
        Child newBornBaby = new NewBornBaby(); // (1)
        Child infant = new Infant(); // (2)
        Child toddler = new Toddler(); // (3)

        newBornBaby.sleep();
        infant.sleep();
        toddler.sleep();
    }
}

실행 결과
=========================================
신생아는 거의 하루 종일 자요
영아부터는 밤에 잠을 자기 시작해요
유아는 낮잠을 건너뛰고 밤잠만 자요

위 코드 예시에선 Child라는 상위 클래스에 일반화시켜 놓은 아이의 동작을 NewBornBaby, Infant, Toddler라는 클래스로 연령별 아이의 동작으로 구체화시켜서 사용을 하고 있다.

 

여기서 중요한 것은 클라이언트(여기서는 ChildManageApplication 클래스의 main() 메서드)는 NewBornBaby, Infant, Toddler를 사용할 때 구체화 클래스의 객체를 자신의 타입에 할당하지 않고, (1) ~ (3)과 같이 Child 클래스 변수에 할당을 해서 접근한다.

 

이렇게 되면 클라이언트 입장에서는 Child라는 추상 클래스만 일관되게 바라보며 하위 클래스의 기능을 사용할 수 있다.

 

이처럼 클라이언트가 추상화된 상위 클래스를 일관되게 바라보며 하위 클래스의 기능을 사용하는 것이 바로 일관된 서비스 추상화(PSA)의 기본 개념이다.

 

 

 

 

728x90