프레임워크는 말 그대로 '뼈대나 근간을 이루는 코드들의 묶음'을 뜻한다. 프레임워크를 이용한다는 의미는 프로그램의 기본 흐름이나 구조를 정하고, 모든 팀원이 이 구조에 자신의 코드를 추가하는 방식으로 개발하는 것이다.
- POJO 기반의 구성
- 의존성 주입(DI)를 통한 객체 간의 관계 구성
- AOP(Aspect-Oriented-Programming) 지원
- 편리한 MVC 구조
- WAS의 종속적이지 않은 개발 환경
스프링은 light-weight 프레임워크지만, 그 내부에는 객체 간의 관계를 구성할 수 있는 특징을 가지고 있다. 스프링은 다른 프레임워크들과 달리 이 관계를 구성할 때 별도의 API 등을 사용하지 않는 POJO(Plain Old Java Object)의 구성만으로 가능하도록 제작되어 있다. 즉, 일반적인 Java 코드를 이용해서 객체를 구성하는 방식을 그대로 스프링에서 사용할 수 있다는 뜻이다.
이것이 중요한 이유는 코드를 개발할 때 개발자가 특정한 라이브러리나 컨테이너의 기술에 종속적이지 않다는 것을 의미하기 때문이다. 개발자는 가장 일반적인 형태로 코드를 작성하고 실행할 수 있기 때문에 생산성에서도 유리하고, 코드에 대한 테스트 작업 역시 유연하게 처리할 수 있다.
- 코드의 내부에서 객체간의 연결을 이루지 않고, 외부에서 설정을 통해서 객체간을 연결하는 패턴
- 컴파일시가 아닌 실행시에 의존 관계가 완성되는 방식
- 스프링의 경우 의존성 주입을 쉽게 적용할 수 있는 프레임워크
- 프레임이 정해져 있고 클래스들을 밖에서 주입시키는 형식
의존성이란 하나의 객체가 다른 객체 없이 제대로 된 역할을 할 수 없다는 것을 의미한다. 예를 들어, 음식점이라면 서빙을 담당하는 직원이 갑자기 하루 못나오는 상황이 있어도 장사는 할 수 있지만, 주방장에게 문제가 생겨서 못나오면 장사를 할 수 없는 상황이 발생한다. 의존성은 이처럼 하나의 객체가 다른 객체의 상태에 따라 영향을 받는 것을 의미한다. 흔히 A객체가 B객체 없이 동작이 불가능한 상황을 'A가 B에 의존적이다'라고 표현한다.
주입(Injection)은 말 그대로 외부에서 '밀어 넣는 것'을 의미한다.
의존성과 주입을 결합해서 생각해보면 '어떤 객체가 필요한 객체를 외부에서 밀어 넣는다'는 의미가 된다. 그렇다면 '왜 외부에서 객체를 주입하는 방식'을 사용할까?
의존성 주입 방식을 사용하려면 오른쪽 그림처럼 추가적인 하나의 존재가 필요하게 된다. 이 존재는 의존성이 필요한 객체에 필요한 객체를 찾아서 주입하는 역할을 한다.
즉, 스프링이라는 프레임워크는 어떤 일을 해야하는지 미리 틀이 정해져있고 실제로 일을 하는 클래스는 바깥에서 주입한다고 이해하면 된다.
Chef 클래스를 다음과 같이 작성한다.
package com.zerock.sample;
import org.springframework.stereotype.Component;
import lombok.Data;
@Component
@Data
public class Chef {
}
Restaurant 클래스는 Chef를 주입받도록 설계한다. 이 때 Lombok의 setter를 생성하는 기능과 생성자, toString() 등을 자동으로 생성하도록 @Data 어노테이션을 이용한다.
package com.zerock.sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import lombok.Data;
import lombok.Setter;
@Component
@Data
public class Restaurant {
@Setter(onMethod_ = @Autowired)
private Chef chef;
}
작성된 코드가 의미하는 것은 Restaurant 객체는 Chef 타입의 객체를 필요로 한다는 것이다. @Component는 스프링에게 해당 클래스가 스프링에서 관리해야 하는 대상임을 표시하는 어노테이션이고, @Setter는 자동으로 setChef()를 컴파일 시 생성한다.
@Setter에서 사용된 onMethod 속성은 생성되는 setChef()에 @Autowired 어노테이션을 추가하도록 한다.
스프링은 클래스에서 객체를 생성하고 객체들의 의존성에 대한 처리 작업까지 내부에서 모든 것이 처리된다. 스프링에서 관리되는 객체를 흔히 '빈(Bean)'이라 한다.
프로젝트의 src 폴더 내에 root-context.xml
은 스프링 프레임워크에서 관리해야 하는 객체(Bean)를 설정하는 설정파일이다.
root-context.xml
을 클릭해서 아래 NameSpaces 탭의 context 항목을 체크한다.
'Source'탭을 선택해서 아래의 코드를 추가한다.
변경된 XML을 저장하고 'Bean Graph'탭을 보면 Restaurant와 Chef 객체가 설정된 것을 확인할 수 있다.
http://www.springframework.org/schema/context
의 역할http://www.springframework.org/schema/context
는 Spring Framework의 XML 설정에서 사용하는 네임스페이스 URI입니다.
XML 파일의 네임스페이스는 해당 XML 문서 내에서 특정 요소와 속성의 집합을 식별하는 데 사용되는 유니크한 식별자입니다. Spring Framework에서는 이러한 네임스페이스를 사용하여 다양한 설정 옵션과 기능을 제공합니다.
특히 http://www.springframework.org/schema/context
네임스페이스는 스프링의 "context" 모듈과 관련된 설정을 담당합니다. 이 모듈은 다음과 같은 기능을 포함합니다:
Annotation-based Configuration: @Component
, @Service
, @Repository
, @Controller
와 같은 주석을 사용하여 빈(bean)을 자동으로 등록하고 의존성 주입을 수행하는 기능을 포함합니다.
Property Placeholder: 속성 값들을 외부화하여 .properties
파일이나 다른 소스에서 불러올 수 있게 합니다.
Internationalization (i18n) and Localization (l10n): 다국어 지원 및 지역화를 위한 설정을 제공합니다.
XML 파일에서 이 네임스페이스를 사용하려면, 보통 아래와 같은 형식으로 XML 문서의 상단에 네임스페이스 선언을 포함시킵니다:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Spring configuration goes here -->
</beans>
여기서 xmlns:context="http://www.springframework.org/schema/context"
부분이 해당 네임스페이스를 'context'라는 접두사와 연결시키며, 이를 통해 해당 네임스페이스의 기능을 XML 문서 내에서 사용할 수 있게 됩니다.
작성한 2개의 클래스와 'root-context.xml'이 어떻게 동작하는지 이해하기 위해서는 스프링과 함께 시간의 순서대로 고민해 보아야 한다.
root-context의 설정 내용이 동작하면서 필요한 인스턴스들(beans)을 생성하고, 의존 관계를 파악해서 주입시켜 주는 방식인데 구체적인 동작은 다음과 같다.
<context:component-scan>
태그의 내용을 통해서 'org.zerock.sample'패키지를 스캔(scan)하기 시작한다.여기서 언급된 "레퍼런스", "객체", "인스턴스"는 모두 맥락상으로 비슷한 의미를 지니고 있습니다. 하지만 기술적인 정확성을 위해 개념을 명확히 구분하면:
객체(Object): 메모리에 할당된 실체를 의미합니다.
인스턴스(Instance): 클래스에서 정의된 것을 토대로 실제 메모리에 생성된 것을 의미합니다. 즉, 객체는 클래스의 인스턴스라고 볼 수 있습니다.
레퍼런스(Reference): 객체의 메모리 주소를 가리키는 변수를 의미합니다. 이 레퍼런스를 통해 객체에 접근할 수 있습니다.
따라서 "스프링은 Chef 객체의 레퍼런스를 Restaurant 객체에 주입한다"에서의 "레퍼런스"는 Chef 객체를 가리키는 주소 값을 Restaurant 객체에 주입한다는 의미입니다.
다만, 실무에서는 종종 이 세 용어를 혼용해서 사용하기도 합니다. 그러나 이러한 기본적인 정의를 이해하고 있으면 혼동 없이 통신할 수 있습니다.
프로젝트 내 'src/test/java' 폴더 내에 'org.zerock.sample.SampleTests' 클래스를 추가한다.
SampleTests 클래스는 spring-test 모듈을 이용해서 간단하게 스프링을 가동시킨다.
@Runwith : 테스트 코드가 스프링을 실행하는 역할을 할 것이라는 것을 표시한다.
@ContextConfiguration : 지정된 클래스나 문자열을 이용해서 필요한 객체들을 스프링 내에 등록한다. @ContextConfiguration에서 사용하는 문자열은 'classpath:'나 'file:'을 이용할 수 있으므로, 자동으로 생성된 root-context.xml의 경로를 지정할 수 있다.
@Log4j : Lombok을 이용해서 로그를 기록하는 Logger를 변수로 생성한다.
@Autowired : 해당 인스턴스 변수가 스프링으로부터 자동으로 주입해 달라는 표시이고, 스프링은 정상적으로 주입이 가능하다면 obj 변수에 Restaurant 타입의 객체를 주입하게 된다.
@Test : JUnit에서 테스트 대상을 표시하는 어노테이션이다. 이는 해당 메서드를 선택해 JUnit Test 기능을 실행한다. assertNotNull()은 Restaurant 변수가 null이 아니어야만 테스트가 성공한다는 것을 의미한다.
아래와 같이 실행해서 테스트 결과를 확인한다.
INFO : org.springframework.beans.factory.annotation.
AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject'
annotation found and supported for autowiring
INFO : com.zerock.sample.SampleTests - Restaurant(chef=Chef())
INFO : com.zerock.sample.SampleTests - --------------------------------
INFO : com.zerock.sample.SampleTests - Chef()
INFO : org.springframework.context.support.GenericApplicationContext
실행된 결과에서 주목해야 할 부분은 다음과 같다.
new Restaurant()와 같이 Restaurant 클래스에서 객체를 생성한 적이 없는데도 객체가 만들어졌다는 점 - 스프링은 관리가 필요한 객체(Bean)를 어노테이션 등을 이용해서 객체를 생성하고 관리하는 일종의 '컨테이너'나 '팩토리'의 기능을 가진다.
Restaurant 클래스의 @Data 어노테이션으로 Lombok을 이용해서 여러 메서드가 만들어진 점 - Lombok은 자동으로 getter/setter 등을 만들어주는데 스프링은 생성자 주입 혹은 setter 주입을 이용해서 동작한다. Lombok을 통해서 getter/setter 등을 자동으로 생성하고 'onMethod'속성을 이용해서 작성된 setter에 @Autowired 어노테이션을 추가한다.
Restaurant 객체의 Chef 인스턴스 변수(멤버 변수)에 Chef 타입의 객체가 주입되어 있다는 점 - 스프링은 @Autowired와 같은 어노테이션을 이용해서 개발자가 직접 객체들과의 관계를 관리하지 않고, 자동으로 관리되도록 한다.
즉, 위의 내용을 정리하면
1) 테스트 코드가 실행되기 위해서 스프링 프레임워크가 동작했고,
2) 동작하는 과정에서 필요한 객체들이 스프링에 등록되었고,
3) 의존성 주입이 필요한 객체는 자동으로 주입이 이루어졌다.
고객(client)의 요청을 중심으로 프로그래밍하는게 어떨까?
AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.
AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진(횡단) 관심사 (Crosscutting Concerns)라 부른다.
위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.
즉, 전통적인 OOP (Object-Oriented Programming)에서는 코드의 재사용성을 증가시키기 위해 객체와 클래스를 사용하지만, 횡단 관심사의 경우 여러 클래스와 메서드에 걸쳐서 반복적으로 나타나는 경향이 있습니다. AOP는 이러한 반복과 중복을 최소화하고, 코드의 모듈성을 향상시키기 위해 도입되었습니다.
AOP의 주요 개념은 다음과 같습니다:
Spring Framework는 Java에서 AOP를 구현하기 위한 널리 사용되는 도구 중 하나입니다. Spring AOP를 사용하면 손쉽게 횡단 관심사를 모듈화하고 애플리케이션의 다른 부분에 적용할 수 있습니다.
메서드 호출 전후로 간단한 로그를 출력하는 Aspect를 만들어봅시다.
<!-- Spring AOP와 AspectJ의 의존성 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
package com.example.demo.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.demo.service.*.*(..))")
public void beforeMethodCall() {
System.out.println("메서드 호출 전 로그...");
}
@After("execution(* com.example.demo.service.*.*(..))")
public void afterMethodCall() {
System.out.println("메서드 호출 후 로그...");
}
}
여기서 execution(* com.example.demo.service.*.*(..))
는 com.example.demo.service
패키지의 모든 클래스와 모든 메서드에 Advice를 적용하겠다는 표현입니다.
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String sayHello(String name) {
return "Hello, " + name + "!";
}
}
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
GreetingService greetingService = context.getBean(GreetingService.class);
greetingService.sayHello("John");
}
}
위 예제를 실행하면 GreetingService
의 sayHello
메서드 호출 전후로 Aspect에 정의된 로그가 출력됩니다.
이렇게 AOP를 사용하면 로깅, 트랜잭션 관리, 보안 등의 공통 기능을 여러 클래스와 메서드에 걸쳐 쉽게 적용할 수 있습니다.