"/projects/{project:[a-z]}/versions"+ : 변수를 정규 분포와 일치시키고 캡처합니다.
antPathMatcher: 문자열 패턴이 정확하게 문자열 경로 일치했을 때 사용되는 방법이다. 효율성이 떨어진 단점이 있다.
GET/POST 요청 둘다 허용된다.
2-2. 코드
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
Controller임을 나타내며 @Component, 즉 spring의 빈으로 등록하기 위한 기본 어노테이션의 확장 형태이다. 그렇기에 @Controller만 작성해도 spring의 빈으로 등록되며 @RequestMapping 같이 사용해 특정 URL 요청 처리하는 핸들러 메서드를 정의한다.
@Controller 없으면 dispatcher-servlet.xml로 설정 하는 과정이 개발자를 고단하게 만든다는 건 이해. 그러나 @Component + @RequestMapping으로 하면 굳이 @Controller가 왜 필요한지 이해 되지 않음. ChatGPT는 웹 컨트롤러를 좀 더 명확히 구분하고 Spring MVC 역할 표현하기 위해 생겼다는데 개발자가 명명규칙을 명확히 작성하는 것과 연관있는건가 싶음.
3-3. 💡@RestController vs @Controller
@RestController = @Controller + @ResponseBody이다. 그래서 클라이언트가 요청한 데이터를 JSON, XML등 형식으로 반환해야 하는 REST API에서 자주 사용된다. 만약 뷰를 반환하고 싶으면 @Controller, 그게 아니라면 @RestController 사용한다.
특성
@Controller(Spring MVC)
REST API(Spring REST)
기본목적
HTML 포함한 웹 페이지 반환
데이터(보통 JSON 또는 XML)만 반환
HTTP 메서드
GET, POST, PUT, DELETE등
GET, POST, PUT, DELETE등
주로 사용되는 @
@Controller, @RequestMapping
@RestController, @RequestMapping
주요목적
웹 애플리케이션에서 사용자 인터페이스 렌더링
RESTful API(REST API를 더 강조한 표현)의 서비스 데이터 처리
3-4. 코드
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
/**
* Alias for {@link Component#value}.
*/
@AliasFor(annotation = Component.class)
String value() default "";
}
4. ModelAndView
4-1. 개념
Model + View, 즉 모델과 뷰를 모두 담을 수 있는 객체다.
ModelAndView mv = new ModelAndView("form") 으로 뷰 이름 modelAndView.addObject("user", user); 로 모델 데이터 담는다.
@Controller 사용시에 이용한다. @RestController는 ResponseEntity 또는 Java 객체 직접 반환한다.
5. RequestParam
5-1. 개념
Http 요청 파라미터 받는 @이다.
@RequestParam("username") 과 request.getParameter("username") 동일하다.
해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.
올바르지 않은 내용이 있다면 의견 남겨주세요. 언제든지 환영입니다.
1. 프론트 컨트롤러
1-1. VER.1
MVC 패턴을 적용하였지만, 하나의 컨트롤러에 공통 + 비즈니스 로직이 같이 존재한다. 하지만 프론트 컨트롤러 패턴을 사용하면 웹 애플리케이션의 구조를 단순화하고, 공통된 처리 로직을 중앙 집중화할 수 있다. 스프링 MVC는 dispatcherServlet이 프론트컨트롤러 패턴으로 구현되어있다.
공통을 집중한 프론트 컨트롤러 패턴은 다음과 같은 장점이 있다
단일 진입점에서 처리하여 공통된 처리 로직을 중앙 집중화할 수 있으므로 코드의 일관성을 유지하고, 중복된 코드를 줄일 수 있다.
package hello.servlet.web.frontcontroller.v1;
import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/* * 하위에 있는 다 들어옴*/
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
/* 해당 url 호출하면 FrontControllerServletV1 이 호출 되야 하므로 */
Map<String, ControllerV1> controllerV1Map = new HashMap<String, ControllerV1>();
/*
* 1. 해당 컨트롤러 호출
* 2. 생성자 > url 주소에 해당하는 Controller 저장
* 2-1. controllerV1Map의 url 주소와 일치하는 ControllerV1이 실행 */
public FrontControllerServletV1() {
controllerV1Map.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
controllerV1Map.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
controllerV1Map.put("/front-controller/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
System.out.println("========= FrontControllerServletV1 ==============");
// 생성자에 일치하는 URL 찾기
/*ControllerV1 controller = controllerV1Map.get(requestURI) 는 곧
* ControllerV1 controller = new MemberFormControllerV1() 와 같다 (url이 /front-controller/v1/members/new-form 일 경우) => 다형성 */
String requestURI = req.getRequestURI();
ControllerV1 controller = controllerV1Map.get(requestURI);
// 만약 없으면
if (controller == null) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(req, res);
}
}
URL 주소를 '/front-controller/v1/*' 로 설정하여 주소가 '/front-controller/v1/save', '/front-controller/v1/list/123' 등 '/front-controller/v1/' 로만 시작한다면 FrontControllerServletV1이 실행된다.
다형성을 이용한 controllerV1Map 생성자에서 url 주소에 해당하는 Controller를 찾는다.
해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.
올바르지 않은 내용이 있다면 의견 남겨주세요. 언제든지 환영입니다.
1. 서블릿 코드
package hello.servlet.domain.member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용
* */
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static Long sequence = 0L; // id가 하나씩 증가하는 것 사용
// 순수하게 만드는 것이므로 싱글톤
private static final MemberRepository instance = new MemberRepository();
public static MemberRepository getInstance() {
return instance;
}
private MemberRepository() {}
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id) {
return store.get(id);
}
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
public static MemberRepository getInstance() {
return instance;
}
getInstance()로 싱글톤 패턴을 구현하였으며, 스프링 빈을 이용하지 않고 순수하게 서블릿을 만들었다. 비즈니스 로직만 구현하면 되니 보다 편하게 구현할 수 있었으나, 자바코드로 html을 만들어 간단한 회원등록 화면임에도 복잡하고 비효율적인 코드를 작성할 수 밖에 없었다. 그래서 자바 코드로 html을 만드는 대신 html 문서에 자바 코드를 동적으로 넣을 수 있다면 편리하지 않을까하고 고민하여 나온 것이 템플릿 엔진이다.
모든 출력을 Java 코드 내에서 문자열로 작성해야 한다는 점에서 오류가 발생하기 쉬웠다. 이때 등장한 JSP는 JAVA 코드를 삽입할 수 있도록 허용하게 했다. 그렇기에 사람들은 데이터베이스 코드, 비즈니스 로직, HTML 마크업등 모든 자바스크립트 코드를 한 곳에서 작성하기 시작했고 이것은 코드 재사용을 불가능하게 하였다. 이러한 구조를 MVC1(Model1)로 정하기도 하였다.
MVC2 패턴은 MVC1의 문제를 개선하였으며 1970년 후반 스몰토크를 연구하던 개발자들과 합의로 인해 나온 패턴이다. 구조는 위의 사진과 같다.
⚠️ 아래 적용한 코드의 Service는 이전에 만든 MemberRepository로 사용하고, Model은 request 내부 저장소를 이용하였다. 그리고 request.getAttribute() , request.setAttribute()를 사용하여 데이터 보관 조회하였다.
해당 내용은 노마드 코더의 강의 및 React 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.
올바르지 않은 내용이 있다면 의견 남겨주세요. 언제든지 환영입니다.
1. Props
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const root = document.getElementById("root");
// prop: 부모 컴포넌트로부터 자식 컴포넌트에 데이터를 보낼 수 있게 해주는 방법
// 버튼을 N개 만든다고 해보자, 이때 버튼의 스타일이 동일하고 글자만 다르다면, TEXT만 바꿔서 만들 수 있도록 수정할 수 있어야 한다.
function Button({berry, onClick}) {
console.log(`${berry} was rendered`);
// button컴포넌트에 던진 berry라는 key 가져오기
// console.log(props.berry);
// return(
// <button>{props.berry}</button>
// )
// shortcut: Button({berry})를 통해 Button(props)의 props.berry 대신 편하게 쓴다
// props로 받아온 onClick을 return을 통해 실제 이벤트 리스터 등록했다.
return (
<button onClick={onClick}>{berry}</button>
)
}
function App() {
const [value, setValue] = React.useState("burabong!");
const handleClick = () => {
setValue("cute burabong🍊");
}
return (
<>
<Button berry={value} onClick={handleClick}/>
<Button berry="banana"/>
</>
)
//<Button berry="{value}" onClick={handleClick}/> 의 onClick은 이벤트 리스너가 아니다. berry와 마찬가지로 props다.
}
ReactDOM.render(<App/>, root);
</script>
</html>
부모에서 자식으로 넘기는 props
props를 통한 렌더링 문제
처음 실행 시 <button> 컴포넌트를 통해 렌더링
버튼 클릭 시 <button> 재렌더링
props 타입 정리
협업 진행 시 해당 props 데이터는 문자만 와야하고, 필수로 넘겨주어야 할 부분들이 존재할 수 있다. 이때, PropsTypes를 이용하여 해당 key의 타입이 어떤 건지 정의해주면 개발자 도구에서 오류로 안내해준다.
effect: useEffect의 첫번째 인자. 정리 함수를 반환할 수 있는 명령형 함수. 해당 함수를 통해 우리는 딱 한번만 실행시키고 싶은 내용을 작성한다.
dependencyList : useEffect의 두번째 인자.[ ] 적힌 값이 변경되는 경우에만 effect 함수가 실행된다. 아무것도 안적으면 최초 1번, [a, b] 둘다 적으면 a or b와 같다. a만 변경되거나 b만 변경되었을때도 해당 effect 함수가 실행된다.
import logo from './logo.svg';
import './App.css';
import Button from './Button';
import { useState, useEffect } from 'react';
function Hello() {
// useEffect의 생성, 해지 되었을 때 이런식으로 동작함을 파악할수 있다.
useEffect(() => {
console.log('created :)');
return () => console.log('destoryed :/');
}, []);
return(<h1>Hello</h1>)
}
function App() {
const [showing, setShowing] = useState(false);
const handleClick = () => (setShowing((prev) => !prev));
return (
<>
{showing ? <Hello/> : null}
<button onClick={handleClick}>{showing ? 'Hide' : 'Show'}</button>
</>
);
}
export default App;
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script>
// 어려운 방식 react의 동작방법을 알기 위한 방식
const root = document.getElementById("root");
// reactJS는 유저에게 보여질 내용을 컨트롤 할 수 있음
// reactJS는 javascript로 그리고 html로 번역
//const span = React.createElement("span", {id: "rabong-span", style: {color: "orange"}}, "hello i'm burabong");
// reactdom은 reactelement를 html로 바꿔준다, reactJs는 엔진
const span = React.createElement("span", null, "hello i'm burabong");
const button = React.createElement("button", {
onClick:() => console.log('im clicked'),
style: {
backgroundColor: 'yellow'
}
}, "Click me"); // {} 프로퍼티에 이벤트도 등록 가능하다.
const container = React.createElement("div", null, [span, button]); // div 안에 span, button 같이 들어가기 위함
ReactDOM.render(container, root); // container root안에 렌더링 해줘
</script>
</html>
버튼 클릭 시 숫자세기 - 복잡한 React 코드
JavaScript 처럼 html을 생성하고 그 다음 이벤트를 추가하는 것이 아닌, React.createElement()를 통해 html 생성 및 프로퍼티, 이벤트를 한번에 만들고 있음을 확인 할 수 있다.
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const root = document.getElementById("root");
// 이렇게 쓰는 방식을 JSX
// 브라우저가 JSX를 이해하기 위해서 Babel 이용해 변환해준다
function Span() {
return (<span>hello i'm burabong</span>);
}
// const Span = () => (<span>hello i'm burabong</span>); 위와 똑같음
const Button = () => (<button style={{
backgroundColor: 'yellow'
}} onClick={() => console.log('im clicked')
}>Click me</button>);
// 컴포넌트의 첫글자는 무조건 대문자, 안그러면 react, jsx는 html 태그라고 인식한다.
const Container =() => (
<div>
<Span/>
<Button/>
</div>
)
ReactDOM.render(<Container/>, root); // container root안에 렌더링 해줘
</script>
</html>
버튼 클릭 시 숫자세기 - JSX를 이용한 React 코드
JSX : JavaScript를 확장한 문법, HTML과 비슷하게 마크업을 작성할 수 있도록 해준다. 또한 CamelCase로 작성해야 하며 변수명에 대시를 포함하거나 class처럼 예약어를 사용할 수 없다. 다만 aria-*, data-* 어트리뷰트는 HTML과 동일하게 대시 사용하여 작성할 수 있다.
Babel: JavaScript 컴파일러, 주로 EMCAScript 2015+ 코드를 이전 버전의 JavaScript로 변환하는 데 사용되는 도구 체인이다. 이런 Babel은 JSX도 변환해줄 수 있다.
개발자 도구 통해서 본 BABEL 컴파일버튼 클릭 시 숫자세기 - 좋은 코드①
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/react@17.0.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const root = document.getElementById("root");
// state는 데이터가 기본적으로 저장되는 곳
function App() {
console.log(React.useState(0));
// 배열을 한번에 빼내기 위해서
// modifier(=setCounter)가지고 state(=counter)를 변경할 때 컴포넌트 재 생성, 새로운 값 가지고 리렌더링되는 것
// 데이터가 바뀔 때마다 컴포넌트 리렌더링하고 UI refreash하는 거다
let [counter, setCounter] = React.useState(0);
const onClick=()=> {
// counter = counter + 1은 내가 원하는 값이 나오지 않을 수 있다. 다른 곳에서 써버릴 수 있으니까
// 이전값을 가지고 값 변화해주고 싶다면 함수를 이용한다.
setCounter((current) => current + 1);
}
return (
<div>
<span>Total clicks: {counter}</span>
<button id="btn" onClick={onClick}>click me</button>
</div>
)
}
ReactDOM.render(<App/>, root);
</script>
</html>
useState:
컴포넌트에 state 변수를 추가 할 수 있는 React Hook
useState(InitialState), 해당 결과값이 배열이므로 const [age, setAge] = React.useState(28); 처럼 배열 구조 분해를 사용하여 변수의 이름 지정하는 것이 규칙이다.
정확히 두개의 값을 가진 배열 반환한다. 첫번째는 초기 state값, 두번째는 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있는 set 함수다. set 함수는 반환값이 없다.
// : string과 같은 타입을 생략해도 된다.
// TypeScript는 처음에 세팅된 값의 타입이 아닌 값을 변경시에 오류를 보여준다.
let itemName: string = '펭수';
let itemPrice: number = 129000;
let membersOnly: boolean = true;
let owner: undefined = undefined;
let seller: null = null;
mySize.push('1234');
// 위와 같이 작성하면 mySize에 [ 5, 6, '55', '1234' ]로 추가가 됨을 확인할 수 있다.
// 이런 내용을 방지하고자 'readOnly'를 추가하면 위와 같이 추가가 불가능하다.
let ourTuple: readonly [number, number, string] = [5, 6, '55'];
ourTuple.push('1234');
// prog.ts(10,10): error TS2339: Property 'push' does not exist on type 'readonly [number, number, string]'.
3. TypeScript 객체
let product: {
id: String;
name: String;
sizes: String[];
membersOnly?: boolean; //?는 optional property
} = {
//id: 1 이렇게 하면 해당 오류 알려준다
id: 'c001',
name: '코드잇 블랙 후디',
sizes: ['M', 'L', 'XL']
}
optional property는 필수로 정의하지 않아도 되는 속성이다. 위의 코드에서 memeberOnly 뒤에 ?를 표시하였는데, 이건 필수로 정의하지 않아도 된다. 만약 없다면 오류 발생한다.
// 속성 정의되지 않았을 때 쓰는 방법
// index 대신 어떤 이름이든 상관 없다. 다만 속성명은 String이고 속성 값이 number라는 정의만 지키면 된다.
const nameAgeMap: { [index: string]: number } = {};
nameAgeMap.Jack = 25; // no error
nameAgeMap.Mark = "Fifty"; // Error: Type 'string' is not assignable to type 'number'.
console.log(nameAgeMap);
해당 내용은 김영한씨의 강의 및 스프링 프레임워크 공식 문서, 공식문서 번역을 참고하여 작성한 글입니다.
올바르지 않은 내용이 있다면 의견 남겨주세요. 언제든지 환영입니다.
1. Lifecycle Callbacks
데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.
아래는 김영한씨의 강의의 예제 코드를 참고하여 우리가 의도하는 결과가 나오는지 테스트해보았다.
public class NetworkClientTest {
private String url;
public NetworkClientTest() {
System.out.println("현재 url은 다음과 같습니다 : " + url);
connect();
call("초기화 연결되었습니다");
}
// 연결함수
public void connect() {
System.out.println("connect, 연결된 url : " + url);
}
public void disconnect() {
System.out.println("disconnect, 연결 끊어버린 url : " + url);
}
// URL 세팅 함수
public void setUrl(String url) {
this.url = url;
}
// 호출함수
// @param msg : 호출하려는 메세지
public void call(String msg) {
System.out.println("call message : " + msg + " url : " + url);
}
}
lifecycle 테스트 결과
나는 객체가 생성되면서, 원하는 url이 세팅되도록 의도했으나 그게 되지 않았다. 당연하다. 스프링 빈은 객체를 생성할 때 생성자 > 세터기반으로 생성하고 그 뒤에 의존관계를 주입한다. 이미 저 코드는 주입까지 모두 마친 후에 내가 초기값을 할당한게 된다. 그럼 connect() 함수가 실행 되기 전, 초기값을 세팅하고 싶으면 어떻게 해야할까? 방법은 아래 3가지로 나타낼 수 있다.
2. InitializingBean, DisposableBean
InitializingBean, DisposableBean 인터페이스들로 초기화 작업 및 연결을 끊은 이후의 작업을 진행할 수 있다.
public class NetworkClientTest implements InitializingBean, DisposableBean {
private String url;
public NetworkClientTest() {
System.out.println("현재 url은 다음과 같습니다 : " + url);
}
// 연결함수
public void connect() {
System.out.println("connect, 연결된 url : " + url);
}
public void disconnect() {
System.out.println("disconnect, 연결 끊어버린 url : " + url);
}
// URL 세팅 함수
public void setUrl(String url) {
this.url = url;
}
// 호출함수
// @param msg : 호출하려는 메세지
public void call(String msg) {
System.out.println("call message : " + msg + " url : " + url);
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet, 초기화 작업 함수");
connect();
call("초기화 연결되었습니다");
}
@Override
public void destroy() throws Exception {
System.out.println("destroy, 연결 파괴될 때 callback");
}
}
InitializingBean, DisposableBean lifecycle 테스트 결과
다만, 위의 인터페이스들은 스프링 전용 인터페이스이며 스프링 공식문서에서는 JSR-250의 @PostConstruct @PreDestroy을 사용하길 권장한다. 해당 @은 스프링 전용 인터페이스가 아니고 자바 표준이며 스프링이 아닌 다른 컨테이너에서도 동작하기 때문이다.
3. @PostConstruct @PreDestroy
public class NetworkClientTest{
private String url;
public NetworkClientTest() {
System.out.println("현재 url은 다음과 같습니다 : " + url);
}
// 연결함수
public void connect() {
System.out.println("connect, 연결된 url : " + url);
}
public void disconnect() {
System.out.println("disconnect, 연결 끊어버린 url : " + url);
}
// URL 세팅 함수
public void setUrl(String url) {
this.url = url;
}
// 호출함수
// @param msg : 호출하려는 메세지
public void call(String msg) {
System.out.println("call message : " + msg + " url : " + url);
}
@PostConstruct
public void init() throws Exception {
System.out.println("PostConstruct 초기화 작업 함수");
connect();
call("초기화 연결되었습니다");
}
@PreDestroy
public void close() throws Exception {
System.out.println("PreDestroy, 연결 파괴될 때 callback");
}
}
@PostConstruct @PreDestroy lifecycle 테스트 결과
@PostConstruct, @PreDestroy가 붙은 메서드는 명시적으로 선언된 callback 메서드와 생명주기의 같은 지점에서 호출된다. @만 붙이면 되므로 매우 편리한 장점을 가지고 있다. 다만 외부 라이브러리에는 적용하지 못하므로, 외부 라이브러리를 초기화, 종료해야한다면 @Bean의 기능을 사용한다.
4. Receiving Lifecycle Callbacks
public class NetworkClientTest{
private String url;
public NetworkClientTest() {
System.out.println("현재 url은 다음과 같습니다 : " + url);
}
// 연결함수
public void connect() {
System.out.println("connect, 연결된 url : " + url);
}
public void disconnect() {
System.out.println("disconnect, 연결 끊어버린 url : " + url);
}
// URL 세팅 함수
public void setUrl(String url) {
this.url = url;
}
// 호출함수
// @param msg : 호출하려는 메세지
public void call(String msg) {
System.out.println("call message : " + msg + " url : " + url);
}
public void init() throws Exception {
System.out.println("init 초기화 작업 함수");
connect();
call("초기화 연결되었습니다");
}
public void close() throws Exception {
System.out.println("PreDestroy, 연결 파괴될 때 callback");
}
}
Receiving Lifecycle Callbacks lifecycle 테스트 결과
@Bean을 통해 정의된 모든 클래스는 일반적인 생명주기 콜백을 지원한다. @Bean은 Spring XML의 bean 엘리먼트 속성인 init-method, destroy-method에 초기화, 파괴용도의 콜백 메서드를 지정할 수 있다. 만약, 기본적으로 'close', 'shoutdown'이라는 이름을 가진 public 메서드를 갖고 있다면 자동적으로 콜백에 참여하게 된다. 호출하기를 원하지 않는다면 @Bean(destroyMethod="") 와 같이 추가한다. @Bean은 기본적으로 (inferred, 추론) 하기 때문이다.
5. Combining Lifecycle Mechanisms
빈(Bean) 생명주기 동작 방법을 3가지를 알아보았다. 만약 하나의 빈에 여러개의 생명주기 동작이 설정되었고, 각기 다른 메서드 이름으로 구성되었을 때 각 메서드는 다음과 같은 순서로 실행된다. 만일 중복된 메서드 이름이 존재한다면 한번만 실행된다.
Initialization methods
Destroy methods
1. @PostConstruct 2. InitializingBean 인터페이스에서 정의된 afterPropertiesSet() 3. 커스텀한 init()
1. @PreDestroy 2. DisposableBean 인터페이스에서 정의된 destroy() 3. 커스텀한 destroy()
프로젝트 하다보면 하나의 객체만 생성하고 그 객체를 공유할 경우가 많다. 그럴 때, 이 싱글톤 패턴을 활용해서 적용할 수 있다. 해당 싱글톤 패턴을 사용하면 객체 공유가 가능하며 여러 고객이 동시에 요청할 때 요청시마다 객체가 생성되는 것이 아니므로 메모리 낭비를 줄일 수 있다. 즉 싱글톤은 JVM당 하나의 인스턴스만 갖도록 하는 디자인 패턴인 것이다.
public class SingleObject {
//create an object of SingleObject
private static SingleObject instance = new SingleObject();
//make the constructor private so that this class cannot be
//instantiated
private SingleObject(){}
//Get the only object available
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
singleton 참조값 일치
다만, 이런 싱글톤 패턴을 사용하기 위해서는 내부에서 private static 변수를 선언하고, 객체를 new() 로 생성할 수 없도록 생성자를 작성해주어야 한다. 이것은 작성 코드가 길어진다는 단점이 된다. 또한 값을 변경할 수 있는 필드가 있을 경우 문제가 발생한다는 큰 단점이 존재한다. 아래 코드는 김영한씨의 강의 중 유지(stateful)하게 설계되었을 때의 문제코드이다.
public class StatefulService {
//상태를 유지하는 필드
private int price;
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
//여기가 문제!
this.price = price;
}
public int getPrice() {
return price;
}
}
하나의 객체를 공유하다보니 내가 원하지 않는 결과가 나왔다. 그렇기에 싱글톤 패턴을 사용하기 위해서는
특정 클라이언트에 의존적인 필드가 있으면 안된다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
가급적 읽기만 가능해야한다.
필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용한다.
2. Singleton Container
스프링 컨테이너는 우리가 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다. 싱글톤 scope로 정의한 bean에 대해 딱 하나의 인스턴스를 만드는 것이다. 그리고 이러한 싱글톤 인스턴스는 싱글톤 bean을 모아두는 캐시에 저장이 되며 요청이 오면 그 캐시에서 인스턴스를 반환해준다. 아래 그림은 스프링 공식 문서의 싱글톤 scope 동작 그림이다.
스프링 공식 문서 - singleton scope 사진
그럼, 우리는 이제 궁금해지기 시작한다. 공식 문서에서 accountDao를 수없이 작성했지만 하나만 생성될 수 있던 이유가 무엇일까
컨테이너 생성 흐름
위의 사진과 같은 흐름으로 컨테이너가 생성이 된다. 또한 scope란 속성이 싱글톤이므로 doRegisterBeanDefinitions에서 싱글톤으로 생성이 되겠다.
<!-- the following is equivalent, though redundant (singleton scope is the default); using spring-beans-2.0.dtd -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
3. Java Based Singleton Container
그럼 java를 기반으로 한 싱글톤은 어떻게 생성이 되는 것일까,
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(getMemberRepository());
}
@Bean
public MemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(getMemberRepository(), getDiscountPolicy());
}
}
다음과 같은 AppConfig.java 에서 new로 객체를 생성해주고 있기 때문이다. 특히 getMemberRepository()는 memberService(), getMemberRepository(), orderService()를 통해서 최소 3번 new로 객체생성하고 있어 궁금증이 생길 수 있다. 정답은 @Configuration으로 가능하다. (사실은 @Configuration 클래스 안에 있는 @Bean 메서드가 처리되는 것이다)
The @Bean methods in a regular Spring component are processed differently than their counterparts inside a Spring @Configuration class. The difference is that @Component classes are not enhanced with CGLIB to intercept the invocation of methods and fields. CGLIB proxying is the means by which invoking methods or fields within @Bean methods in @Configuration classes creates bean metadata references to collaborating objects. Such methods are not invoked with normal Java semantics but rather go through the container in order to provide the usual lifecycle management and proxying of Spring beans, even when referring to other beans through programmatic calls to @Bean methods. In contrast, invoking a method or field in a @Bean method within a plain @Component class has standard Java semantics, with no special CGLIB processing or other constraints applying.
공식문서에 보면, spring 컴포넌트의 @Bean 메서드는 spring이 @configuration 클래스에 있는 @Bean 메서드와는 다르게 처리된다. 차이점은 메서드나 필드 호출을 가로채기 위한 @Component 클래스에 CGLIB가 사용되지 않는다는 점이다.
CGLIB는 바이트 코드 생성 라이브러리로 Java 바이트 코드를 생성하고 변환하기 위한 고급 API이다. 동적 프록시 객체를 생성하고 필드 액세스를 가로채기 위해 AOP, 테스트, 데이터 액세스 프레임워크에서 사용된다고 한다.