Recoil이란?

React.js를 만든 Facebook에서 만든 전역상태관리 라이브러리. 이 이상의 설명은 필요없을 듯 하다.

 

전역 상태란?

컴포넌트에서 쓰이는 상태값(state)가 아닌, 이곳저것에서 전역적으로 쓰이는 상태(state)를 말한다.

 

전역 상태를 사용하는 이유

굳이 안 써도 되긴 한다. 하지만 그렇게 된다면 서로 다른 컴포넌트끼리 특정한 값(= 상태 = state)를 공유하려면 공통된 상위 컴포넌트로부터 prop을 계속 전달하는 방법을 써야 하는데,

 

  • 중간에 거치는 컴포넌트들마다 하나하나 prop을 전달하게끔 코드 작성하는거 귀찮음
  • 중간에 거치는 컴포넌트들은 전달받은 prop을 하위 컴포넌트로 전달만 하고 사용은 안 할 수도. 즉 비효율적
  • 중간에 거치는 컴포넌트들이 많아지면 나중엔 prop을 추적하기가 힘들어짐

 

이런 문제들이 있다. 이 때 전역적으로 쓰는 상태를 만든다면, 어느 컴포넌트에서나 전역 상태에 접근가능하므로 위에서 말한 불편들이 해소될 수 있다!

 

그럼 수많은 전역관리 툴 중 Recoil을 사용하는 이유?

일단 따지고보면 context API는 전역상태관리를 해주는 기능이라기보다는 단순히 전역적으로 상태를 공유하게끔 해주는 기능이라고 보는게 정확하다. context API를 사용해서도 전역상태값을 변경하는 건 물론 가능하지만, context API를 사용해 상태값을 변경하면 provider로 감싸져있는 컴포넌트들이 죄다 렌더링된다는 치명적 단점이 있다! (provider 하위에서 context를 구독하는 모든 컴포넌트는 provider의 value가 바뀔 때마다 다시 렌더링된다는 말)

 

// ContextTest.jsx

export const myContext = createContext("JOFE");

const ContextTest = (props) => {
    const [name, setName] = useState("JOFE");
    
    return (
        <myContext.Provider value={[name, setName]}>
            <Welcome />
            <Hello />
        </myContext.Provider>
    )
}

 

// Welcome.jsx

const Welcome = (props) => {
    const [name, setName] = useContext(myContext)
    return (
        <div>
            <button onClick={() => {setName("Nunu")}}>이름바꾸기</button>
            {name}
        </div>
    )
}

 

// Hello.jsx

const Hello = (props) => {
    useEffect(() => {
        console.log("Hello 렌더링")
    })

    return (
        <>
        </>
    )
}

 

myContext라는 context의 value로 name이란 상태와 name을 변경해주는 세터함수를 넣어줬다. 그리고 이 context를 Welcome컴포넌트에서 사용하는 모습이다. Hello컴포넌트는 useEffect를 사용해서 렌더링될때마다 console에 Hello 렌더링이란 문구를 찍어준다.

 

 

이름바꾸기를 하면?

 

 

myContext를 사용하지 않는 Hello컴포넌트도 단순히 provider 안에 있다는 죄(?)로 다시 렌더링이 되는 모습을 확인할 수 있다..!

 

바로 여기서 Recoil이 이런 문제들을 해결해줄 수 있기 때문에 사용한다. 하단에서 보여드림.

 

Recoil 기초적인 사용법

일단 상위 컴포넌트에서 RecoilRoot란 컴포넌트로 애들을 감싸주면 된다.

 

import { RecoilRoot } from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <RecoilTest />
    </RecoilRoot>
  );
}

 

그리고 atoms.js파일을 만들어서 요로코롬 atom들을 만들어준다.

※ atom = Recoil에서 사용하는 상태들의 명칭이자 단위라고 생각하면 됨. 이 atom들의 key들은 각각 고유한 값이어야 함! 중복되면 안 된다는 뜻

 

// atoms.js

import { atom } from "recoil";

export const nameState = atom({
    key: "name",
    default: "JOFE"
});

 

참고로 굳이 atoms.js에 만들 필요는 없는데 이렇게 전역상태들을 한 파일에 관리해주는 게 더 편하다고 한다.

 

// RecoilTest.jsx

const RecoilTest = (props) => {
    return (
        <div>
            <Welcome />
            <Hello />
        </div>
    )
}

 

 

// Welcome.jsx

const Welcome = (props) => {
    const [name, setName] = useRecoilState(nameState)
    return (
        <div>
            <button onClick={() => {setName("Nunu")}}>이름바꾸기</button>
            {name}
        </div>
    )
}

 

요로코롬 useRecoilState를 사용해서 React의 hook마냥 쓸 수 있다! React스럽다는게 최고 장점인 듯. 참고로 세터함수는 필요없고 값만 읽고 싶다면 useRecoilValue를 쓰면 된다.

 

암튼, 이렇게 하면

 

 

 

요로코롬 Hello컴포넌트는 리렌더링되지 않는다! 오직 해당 atom을 사용하는 애들만 바뀐다.

 

 

※ 참고한 글https://hong-jh.tistory.com/entry/Context-API%EB%8A%94-%EC%99%9C-%EC%93%B0%EA%B3%A0-%EA%B7%B8%EB%A0%87%EB%8B%A4%EB%A9%B4-Redux%EB%8A%94-%ED%95%84%EC%9A%94%EC%97%86%EC%9D%84%EA%B9%8C

 

Context API는 왜 쓰고, 그렇다면 Redux는 필요없을까?

안녕하세요 오늘은 제가 평소에 궁금했던 주제로 글을 써보려고 합니다. Context API의 기능과 왜 사용하는지, Context API의 리렌더링 이슈란 무엇인지 그리고 Redux를 대체할 수 있는지. 위 내용을 설

hong-jh.tistory.com

 

@Value를 사용해 application.properties에 적은 값을 가져오는 과정, 구체적으론 jwt secret key를 가져오는 과정에서 null값을 가져오는 문제가 생겨버렸다.

 

이상하게도 AccountController에서 jwt관련 기능들을 쓸 때는 null이 아니라 제대로 된 값을 가져오는데, 필터 쪽에서 jwt 토큰 인증을 할 때에만 null로 가져오는 게 문제였다..

 

원인은 찾아보니 알 수 있었다. 바로 AccountController에서는 jwt key를 담는 멤버변수가 있는 객체가 빈으로 등록된 애였지만, 같은 jwt key를 담는 멤버변수를 가지는 필터는 빈으로 등록된 애가 아니었던 것

 

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable() // CSRF 공격에 대한 방어를 해제
                .cors().and() // cors 허용
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 서버를 stateless하게 유지 즉 세션 X
                .and()
                .formLogin().disable() // 시큐리티가 기본 제공하는 로그인 화면 없앰. JWT는 로그인과정을 수동으로 클래스로 만들어야 하니까
                .httpBasic().disable() // 토큰 방식을 이용할 것이므로 보안에 취약한 HttpBasic은 꺼두기
                .authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다
                .requestMatchers("/account/signup", "/account/login", "/").permitAll() // 요 세 놈에 대한 요청은 인증없이 접근 허용
                .anyRequest().authenticated() // 나머지에 대해선 인증을 받아야 한다.
                .and()
                // 여러 필터들 중 UsernamePassword필터 앞에 내가 만든 필터를 둔다. 이렇게 하면 커스텀 필터로 인가인증을 다룰 수 있음
                .addFilterBefore(new JwtAuthenticationFilter(tokenProvider),
                        UsernamePasswordAuthenticationFilter.class)
                .build();
    }

 

보이다시피 new 연산자를 통해 새로운 필터객체를 만들어서 끼워주는 모습을 볼 수 있다. 이 필터객체가 @Value 어노테이션을 통해 jwt key를 가져와야 하던 놈이다..근데 빈이 아니라 못 가져오던 것! 뭐, 해결법은 간단하다. 필터를 빈으로 만들어 끼우면 된다! 

 

private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf().disable() // CSRF 공격에 대한 방어를 해제
                .cors().and() // cors 허용
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 서버를 stateless하게 유지 즉 세션 X
                .and()
                .formLogin().disable() // 시큐리티가 기본 제공하는 로그인 화면 없앰. JWT는 로그인과정을 수동으로 클래스로 만들어야 하니까
                .httpBasic().disable() // 토큰 방식을 이용할 것이므로 보안에 취약한 HttpBasic은 꺼두기
                .authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다
                .requestMatchers("/account/signup", "/account/login", "/").permitAll() // 요 세 놈에 대한 요청은 인증없이 접근 허용
                .anyRequest().authenticated() // 나머지에 대해선 인증을 받아야 한다.
                .and()
                // 여러 필터들 중 UsernamePassword필터 앞에 내가 만든 필터를 둔다. 이렇게 하면 커스텀 필터로 인가인증을 다룰 수 있음
                .addFilterBefore(jwtAuthenticationFilter,
                        UsernamePasswordAuthenticationFilter.class)
                .build();
    }

 

 

결론!

@Value어노테이션을 통해 값이 멕여지는 멤버변수를 가지는 객체는 빈으로 등록돼있어야 값을 잘 가져온다.


근데 왜 Bean으로 등록해야 가져와지는 거지?

결론적으로 Bean 생성과정에서 @Value가 inject되기 때문..

구체적인 과정들은 현재 내 수준에선 뭔 말인지 잘 모르기 때문에 생략(ㅠ)

 


 

참고한 링크

https://wildeveloperetrain.tistory.com/143

 

@Value 어노테이션 null이 나오는 문제 해결 방법

@Value annotation을 사용하여 properties에 있는 메타 정보 값을 가져오는 과정에서, 값이 null으로 들어오는 문제를 해결하며 기록한 내용입니다. @Value Annotation 쉽게 @Value 어노테이션은 데이터베이스

wildeveloperetrain.tistory.com

https://duooo-story.tistory.com/32

 

@Value는 어디서 set이 되는걸까?

프로젝트에서 reousrce로 등록한 데이터를 @value를 통해서 데이터를 받아 사용하고 있습니다. 이 기능 덕분에 각 존별로 다른데이터를 코드의 분기처리없이 사용하고 있습니다. @Configuration public cla

duooo-story.tistory.com

 

 요즘 들어 많이 마주치는 단어 중 하나는 DI, Dependency Injection이다. 말 그대로 번역하면 "의존성 주입"이고, 좀 더 풀어서 해석하면 "의존관계를 외부에서 넣어주는"이라는 문장이 된다. 책이나 강의를 통한 예제를 통해서는 단순히 어떤 객체가 가지는 멤버변수가 있을 때, 자신 스스로 멤버변수를 만들어서 설정하는게 아니라 외부에서 이미 만들어진 녀석을 갖고 와서 쓰는 느낌으로만 알고 있었다. 그러나 DI를 나 스스로가 정말 무엇을 말하는지 알고 있는가?라고 물으면..음 글쎄다.

 

public class MemberService {
    private final MemberRepository memberRepository;
    
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

내가 아는 DI는 단순히 이렇게 "외부에서 만들어진 녀석을 내부로 갖고 와서 쓰게 해주는 것" 이다

 

이번 기회에 의존성 관계가 뭔지 확실히 정리해보자.


의존성(의존 관계)란

public class MemberService {
    private final MemberRepository memberRepository;
}

 

MemberService가 멤버변수로 MemberRepository를 가지는 모습이다. MemberService의 메서드들은 MemberRepository라는 멤버변수를 다루는 행동들을 할 것이다. 당연하게도, MemberRepository라는 놈이 변하면 그에 따라 MemberRepository를 다루는 MemberService에게도 영향이 미칠 것이다. 때문에 이런 관계에서, "MemberService는 MemberRepository에게 의존한다"라고 표현한다.

 

MemberService가 MemberRepository를 가진다

당연히 MemberService가 MemberRepository에 의한 영향을 받는다

→ MemberService가 MemberRepository에 의존한다

 

인 것이다. 하지만 나는 소프트웨어학과이므로 좀 더 추상적인 수준에서 "의존관계(의존성)"를 설명할 수 있어야 한다.

추상적은 수준에서의 "의존관계"란,

 

"두 객체 사이에서 한 객체가 변하면 다른 객체에게 영향이 가는 관계, 즉 변경에 의한 영향을 받는 관계"

 

라고 표현할 수 있을 것이다. 영향을 받는 객체가 영향을 발생시키는 객체를 의존한다고 표현하는 것이고. 구체적인 설명으로는 한 객체가 다른 객체의 메서드를 쓰는 관계, A가 B를 사용해야만 A의 역할을 수행할 수 있는 관계..라고도 볼 수 있다.

 

 

주입(Injection)이란

이건 단어 그대로 보면 된다. 외부에서 꽂아넣는거다.

 

public class MemberService {
    private final JofeMemberRepository memberRepository;
    
    public MemberService() {
        this.memberRepository = new JofeMemberRepository();
    }
}

 

위 코드는 JofeMemberRepository필드를 클래스 내부에서 스스로 찍어내고(?) 있다.

 

public class MemberService {
    private final JofeMemberRepository memberRepository;
    
    public MemberService(JofeMemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

위 코드는 생성자를 통해(setter메서드 등으로 하는 것도 상관없음) 외부에서 JofeMemberRepository를 받아와서 자신의 필드로 세팅하는 모습이다. 외부에서 가져온다 → 외부에서 "주입"한다.

 

이것을 주입이라고 하는 것이다. 

근데 굳이 왜 주입하는거지? 어차피 내부에서 찍어내나 외부에서 주입하나 어차피 다른 거 없지 않아?

 

그것에 대한 답은 "역할을 분리하기 위해서"이다.

생성과 사용에 대한 관심을 분리하여 생성에 대한 책임은 다른 누군가에게 위임하고, 나는 사용하는 역할을 맡겠다는 것. 쉽게 말해서

 

"만드는 건 너가 해. 쓰는 건 내가 할게"

 

라는 거다. 또한 이렇게 책임을 짬때리는위임하는 동시에 필요에 따라 객체가 생성되는 방식을 선택할 수 있게 된다! 결국엔 의존하는 관계에서 가지던 일종의 강한 결합을 느슨하게 만들게 되고 이것을 통해 설계의 유연성을 가질 수 있기 때문에 굳이 주입하는 식으로 만드는 것.

 

 

이렇게 의존관계를 주입하는 것, 즉 외부에서 끼워넣는 것을 의존성 주입이라고 한다. 그러나! 이렇게 단순히 의존관계를 주입하는 것은 진정한 의존성 주입(DI)이 아니다.

(아니 의존관계를 주입하는 것을 의존성 주입이라고 부르지 않는다니,,이게 뭔말일까?)

 

즉 단순히 외부에서 만들어진 녀석을 내부로 가져온다고 해서 죄다 DI라고 하진 않는 것.

하나 더 알아둬야 할 것이 있다. 바로, 의존성 분리다.

 

 

의존성 분리란

앞서 설명했듯, 의존성(의존관계)란 변경에 의한 영향을 받는 관계를 말한다. 이것을 분리한다는 것은, 한마디로 변경에 의한 영향을 안 받게 하겠다! 라는 거다.

 

물론 A가 B에게 의존하는 관계에서 B가 b라는 메서드를 가지고 있다고 할 때, b메서드의 기능을 바꾼다고 하면 당연히 A에게도 영향이 간다. 여기서 말하고자 하는 영향은 이런 영향이 아니라, 바로 A가 이제부터는 B가 아니라 C라는 모듈을 사용하기로 변경한 경우를 의미한다고 보면 된다. 이 경우 A는 B를 사용하던 코드를 싹 다 바꿔야 한다. 

 

이는 A와 B가 강하게 결합돼있기 때문에 발생한다. 다르게 표현하자면 A가 B를 강하게 의존하기 때문, 즉 A가 B를 너무나도 잘 알기 때문에(?) 발생한다. 둘의 의존관계의 끈끈한 정도가 상당히 센 것이다.(너 없음 난 아무것도 못해~의 느낌..)

 

의존성을 분리한다는 것은 이러한 관계를 끊어낸다는 것으로, 아예 손절친다는게 아니라 그 관계의 끈끈한 정도(?)를 느슨~하게 만든다는 의미다. (강한 결합 느슨한 결합이 되게 추상적인 표현이라고는 생각하는데, 이 말 말고 더 느낌있는 말이 없는 것 같다)

 

이에 대한 전통적인 예시로 연극팀 예시가 있다. 어떠한 배역이 있고 그 배역을 맡은 배우가 있을 때, 내가 연극팀을 운영하는 입장이라면 배우가 아니라 배역에 집중해서 기획을 해야 한다. 배우에 집중한다고 하면(이 경우가 강한 결합!!) 그 배우가 아파서 연극에 못 온다고 하면 그냥 망하는거다. 그러나 특정 배우가 아닌 배역에 집중한다면(이 경우가 느슨한 결합!!) 실제로 이 일을 맡은 배우가 누군지 몰라도 "이걸 누가 하는지는 모르겠는데, 이 역할 맡은 배우는 이렇게 연기하면 돼!"를 알고 있는 거니까 상관없는거다. 즉 배우가 바뀌거나 아프거나 뭐 그런 것에 영향을 받지 않게 되는 것. 

 

연극팀 인원들이 원래는 배우에게 의존하는 관계를 더 이상 배우가 아니라 배역에게 의존함으로써, 연극팀과 배우간의 사이가 상당히 느슨해진다! 라고 표현가능하다. 이렇게 의존관계가 느슨해지는 것을 다른 관점에서 보면 연극팀이 배우가 아닌 배역에 의존함으로써 기존에 가지던 배우에게 의존하던 관계가 분리되기 때문에 "의존성이 분리된다"라고 볼 수 있는 거다. 

 

이렇게 특정 구현체(배우)가 아니라 인터페이스(배역)에 의존함으로써 의존성 분리가 가능하다. 이를 "의존관계 역전 원칙(의존성 뒤집기 원칙, Dependency Inversion Principle)"을 적용함으로써 의존성을 분리한다고 표현한다.

 

 

의존관계 역전 원칙

일단 이름은 간지난다. 처음부터 직접적으로 와닿게 설명하자면 "구상 클래스가 아닌 추상 클래스에 의존하게 만드는" 원칙을 말한다. 조금 더 파헤치면, 고수준 모듈이 저수준  모듈에 의존하면 안 된다는 뜻이 담겨 있다. 

 

구상 클래스가 아닌 추상 클래스에 의존하게 함으로써, 고수준 모듈가 저수준 모듈에게 의존하는 전통적인 의존관계를 뒤집히고 고수준 모듈이 저수준 모듈의 구현체들로부터 독립되게 할 수 있게 된다. 즉 의존성을 분리시킬 수 있다!

 

한마디로 저수준 모듈이 고수준 모듈에게 의존하도록 뒤집으라는건데..이걸 바로 구상 클래스가 아닌 추상 클래스에 의존하게 함으로써 뒤집으라는 얘기다.

 

원래 이랬던 거를
이렇게 역전, 즉 뒤집으라는 거

 

 

※ 여기서 고수준/저수준은 프로그래밍 언어를 말할 때의 고수준(사람에 가까운)/저수준(컴퓨터에 가까운)과 같은 의미다. 고수준 모듈이 어떤 의미있는 기능을 제공하는 모듈이라 한다면, 저수준 모듈은 고수준 모듈의 그 기능을 구현하는 실제적인 역할을 맡는 애들이라고 보면 된다. MemberService의 기능이 실제적으론 MemberRepository의 기능들을 통해 이루어진다면 MemberService가 고수준, MemberRepository가 저수준이 된다. 반대로 MemberRepository의 메서드들은 실제적으로 그들을 구현하는 구현체(ex: JofeMemberRepository)에서 구현되므로 이들사이는 MemberRepository가 고수준, JofeMemberRepository가 저수준이 된다.

 

MemberService는 이제 추상화된 MemberRepository에만 의존한다. 뿐만 아니라 다양한 MemberRepository구현체들도 역시 추상화된 MemberRepositiry클래스에 의존하게 된다. MemberService입장에서는 MemberRepository가 저수준 모듈들이지만, 기존에 의존하던 JofeMemberRepository입장에서는 MemberRepository라는 추상 클래스가 자신보다 고수준이다. 왜냐하면 그 놈의 인터페이스를 실제로 구현하는 역할을 JofeMemberRepository가 하니까. 

 

public class MemberService {
    private final MemberRepository memberRepository;
    
    public MemberService(MemberRepository concreteMemberRepository) {
        this.memberRepository = concreteMemberRepository;
    }
}

 

실제 memberRepository필드(= 배우)로 어떤 놈이 들어올지는(= 주입될지는) 모른다. 그래도 그 놈이 MemberRepository(= 배역)라는 역할을 맡은 것은 알고 있다. 때문에 실제 배우가 누가 되든 나는 상관없다.

 

이렇게 의존성 분리를 해주면서, 외부에서 의존관계를 주입받는 것. 이게 진정한 Dependency Injection이다. 단순히 외부에서 때려넣는다고 죄다 dependency injection이 아님!!

 

마지막으로 이 DI의 장점을 훑고 가면 다음과 같다.

 

  • 의존성이 줄어든다 : 의존성 분리를 해줬기 때문에, 의존대상이 바뀌어도 그로 인한 코드의 수정을 안 해도 된다. 즉 배우가 아닌 배역에 의존하니까 배우로 누가 들어오든 변하는 건 없다. 어차피 얘가 할 일은 정해져있으니까. 변경에 유연해진다고도 표현할 수 있겠다.
  • 재사용성이 높아진다 : 의존관계를 가지는 객체를 내부에서 만드는게 아니라 외부에서 만들어진 애를 받는거니까, 다른 클래스에서도 이 놈을 쓸 수 있다
  • 테스트하기 좋아진다 : 외부에서 주입받는 거니까, 주입되는 놈의 테스트를 주입받는 애의 테스트와 분리해서 가능하다. 
  • 가독성이 좋아진다 : DI를 하는 이유가 아까 말했듯 역할의 분리니까, 기능들이 분리되므로 자연스레 가독성이 높아진다.

 

 

 

스프링 공부하면서 많이 마주치는 키워드들이 있었다. DI, Ioc, 서블릿.. 등등

이런 것들을 짚고 넘어가야 할 것 같다는 생각이 들었다. 야생으로 학습 중인 만큼 지금 당장 이해 안 돼도 넘어가야 하는 부분들이 있겠지만, 그렇다고 다 넘기는 건 아닌 것 같다... 일단 한 번 짚어보고, 지금 당장 다 짚일 것 같지 않으면 넘기고 오 좀 된다 싶으면 짚고 가는게 좋을 듯 함.

그래서 서블릿이란 놈을 한 번 패보기로 했다.


배경

초창기 웹 프로그램은 정적 데이터만 전달 가능했다. 클라이언트가 어떤 걸 요청하면 웹서버가 정적데이터를 응답하는 식. 이게 끝

근데 이제 사용자 요청에 따라 다른 처리, 즉 동적인 처리를 해주고 싶었던 거다. 그걸 위해서 '웹 어플리케이션 프로그램'을 만들어 기존에 존재하던 웹 서버에 붙이고 싶은 거라고 보면 된다.

 

이걸 위해서 CGI가 등장했다. Common Gateway Interface약자로, 웹서버와 앞서 말한 웹 어플리케이션 프로그램 사이의 규약(인터풰이스)이다. C, PHP등으로 요놈의 구현체를 만든다. 이 구현체들은 결국 쉽게 말해서 동적 데이터를 처리해주는 놈, 즉 웹 어플리케이션 프로그램이다.

 

 

그래서 예전과 달리 '동적인 처리'를 해줄 수 있게 됐다. 사람들이 CGI를 많이 활용하게 됐으니까.

근데 문제가 많았던 거다. CGI가 많은 사용자를 처리하기엔 힘들었던 것.

 

  • 클라이언트로부터 request가 들어올 때마다 그 놈들 하나하나마다 웹서버에서 프로세스를 만들어 처리. 프로세스니까 당연히 비용이 비쌌음
  • request들에 대해 같은 CGI 구현체를 써도 프로세스들이 다르면 여러 개의 구현체를 사용해야 됐음. 당연히 비효율적이었음

 

이를 해결하기 위해서 프로세스가 아니라 쓰레드를 만들었다. 그리고 같은 종류의 여러 CGI구현체를 만드는 몹쓸 상황을 막기 위해 CGI구현체를 싱글턴으로 만들었고.

 

이 싱글턴이 바로 서블릿!! 클라이언트로부터 request가 들어올 때마다 쓰레드가 생기고, 이 쓰레드를 통해 싱글턴 CGI구현체에게 동적인 처리를 해달라하는데 이걸 해주는 그 놈을 서블릿이라 부르는 것. 즉 서블릿은 자바로 구현된 CGI기도 한 거다.

 

결국 서블릿은

 

= 클라이언트의 요청을 동적으로 처리할 때 쓰이는 자바 기반의 웹 애플리케이션 프로그래밍 기술(인터페이스임)

= 동적 컨텐츠를 만드는데 사용되는 놈!

 

이라고 말할 수 있다


좀 더 뜯어보기 - 동작 방식

서블릿이 그래 동적인 컨텐츠를 만드는 데 쓰이는 놈이란 건 알겠다. 웹서버가 이 놈한테 말을 건네서 이 서블릿이란 놈이 동적인 처리를 해주는 거구나. 

 

그 과정을 좀 더 뜯어본다.

 

HTTP request, response를 서블릿(얘 자체는 역시나 인터페이스)의 메서드들을 통해 편하게 다룰 수 있다고 한다.

 

 

httpServletRequest = 서블릿 컨테이너가 서블릿에게 전달하는 때 담는 봉투

httpServletResponse = 서블릿이 서블릿 컨테이너에게 돌려줄 때 담아보내라고 지정하는 봉투

 

 

  1. 사용자가 url입력 
  2. HTTP request가 웹서버로 전달됨
  3. 웹서버는 이 요청이 정적 자원을 요청하는지 판단(정적 자원이면 그대로 정적 자원 주면 됨)
  4. 동적인 처리가 필요하면 그 요청을 그대로 was한테 짬때림. 
  5. was의 웹 컨테이너(= 서블릿 컨테이너)가 이를 받고, 처리하기 위한 쓰레드를 만듦
  6. 그리고 컨테이너가 HttpServletRequest, HttpServletResponse객체를 만듦. HttpServletRequest객체로는 사용자가 요청한 내용을 편하게 다루고, HttpServletResponse객체에는 응답할 내용을 편하게 작성 가능
  7. 컨테이너가 사용자가 입력했던 url이 어느 서블릿에 대한 요청인지 찾고(by web.xml ), 걔를 호출. 이 때 아까만든 두 객체를 서블릿에게 선물로 줌
  8. 그 서블릿의 service메서드를 통해 요청이 처리됨! 즉 service메서드에 작성한 코드들이 실행되는 것.
  9. 이 때 아까 받은 request객체를 사용하고, 응답할 내용은 아까 받은 response객체에 저장하는 것.
  10. 또한 service메서드를 호출한 후 클라이언트가 보낸 요청이 GET인지 POST인지에 따라 doGet() 또는 doPost() 호출
  11. 이를 다시 클라이언트에게 최종 결과 응답 후, HttpServletRequest, HttpServletResponse는 삭제

 

※ service메서드를 호출할 때 HttpServletRequest, HttpServletResponse객체를 넘기는 것임! 즉 서블릿이 만들어질때 이 두 놈을 넘기는게 아니라 service메서드를 호출할 때 두 놈을 선물로 주는 것임에 유의

 

※ 톰캣은 was면서 서블릿 컨테이너의 기능도 제공한다고 함!


나아가기 - 스프링 web MVC와 서블릿

그러나..서블릿 역시 문제가 있었던 것이었다. 

 

앞서 설명했듯 사용자가 입력한 url별로 서블릿이 매핑된다. 10개의 각기 다른 url들이 들어오면 10개의 서블릿들이 매핑되는 것! 그럴 때마다 서블릿들이 가지는 "공통된 로직"이 반복돼서 실행된다는 문제점이 있었다. 즉 개발 측면에서 상당히 비효율적이었음.

 

이런 점을 해결하기 위해, 클라이언트로부터의 요청을 받는 서버의 앞쪽에 모든 요청을 받는 하나의 서블릿을 두기로 했다. 그 컨트롤러가 "공통된 로직"을 수행하게 하고, 핵심 비즈니스 로직을 다른 핸들러들에게 위임하는 구조로 바꾼 거다!

 

원래는 이렇게 했는데
이렇게 바꿔줬다는 거!

 

이런 방식을 Front Controller Pattern이라고 한다. 하나의 서블릿(Dispatcher Servlet)으로 모든 요청을 받게 했으니, 요청의 진입점이 같아져 관리가 보다 더 수월해진다는 장점이 있다. 또한 각 서블릿마다 가지는 공통로직을 한 곳에서만 처리함으로써 중복되는 로직의 작성도 방지하게 된다.

 

(디테일하게 디스패처 서블릿이 요청을 처리하는 과정은 본 글에선 다루지 않음)

 

결국엔 이런 방식(디스패처 서블릿이 모든 요청을 받고 공통로직들을 처리하고..)을 스프링이 사용하는 덕분에, 개발자는 핸들러(즉 컨트롤러)에만 집중하면 되도록 발전해왔다..라고 이해하면 될 듯 하다. 

스터디를 진행하던 중 멘토님이 Lombok라이브러리를 사용해서 스터디를 진행하셨다. 하지만 스프링에 막 발을 들인 나는 롬복이 뭔지 모른다. 그래서 정리해봤다.


롬복?

어노테이션 기반으로 코드를 자동완성해주는 라이브러리라고 한다. 개발자 편의를 위해 쓰는 라이브러리인 듯 하다. 다른 언어도 마찬가지지만 자바 언어 역시 기계적으로 작성해야 하는 코드들이 상당히 많이 생기는데 그런 부분들을 자동화해주는 라이브러뤼. 사용한다면 귀찮은 부분들을 작성해주는 걸 편하게 할 수 있을 뿐더러 코드의 길이 자체가 줄어드는 효과를 얻는다는 이점이 있다.

 

 

활용예시 - Getter, Setter

클래스를 만들었다. 이 놈이 갖는 필드 하나하나에 대해 getter, setter를 하나하나 만들어줘야 하는데 여간 귀찮은 게 아니다. 이럴 때 롬복을 이용해 Getter, Setter어노테이션을 주면 지가 알아서 필드들에 대한 getter, setter들을 만들어준다.

 

@Getter
@Setter
public class Member {
    private Long id;
    private String name;
}

 

참고로 클래스 이름 위에 이 어노테이션을 작성하면 모든 필드들에 대해 적용되고, 필드 이름 위에 작성하면 해당 필드에만 적용된다.

 

 

활용예시 - NoArgsConstructor

빈 기본 생성자를 만들어준다.

 

@NoArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member() {
   }
   */
}

 

 

활용예시 - AllArgsConstructor

모든 필드에 대한 생성자를 만들어준다. (DI(Dependency Injection)를 해주는 식으로)

 

@AllArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

활용예시 - RequiredArgsConstructor

특정 필드들에 대한 생성자를 만들어준다. 도대체 어떤 필드들에 대해 해주냐? final이 붙은 필드들, 그리고 @NonNull 어노테이션이 붙은 필드들에 대해서 자동으로 생성자를 만들어준다. AllArgsConstructor와 마찬가지로 DI(Dependency Injection)을 해주는 생성자를 만들어준다. 

 

※ NonNull 어노테이션 : 롬복에서 쓰는 어노테이션으로, 얘가 멕여진 필드가 null이 되면 NullPointerException을 일으킨다.

 

@RequiredArgsConstructor
public class Member {
    private final Long id;
    @NonNull
    private String name;
    private int age;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

이 외에도 어노테이션들이 많다. 클래스에 대한 equals함수와 hashCode함수를 자동으로 만들어주는 @EqualsAndHashCode, 필드들을 기반으로 ToString메서드를 자동으로 만들어주는 @ToString, 객체 생성에 Builder패턴을 적용해주는 @Builder.. 암튼 다양하다. 그때그때 찾아가며 공부하면 될 듯.


스프링에서 @RequiredArgsConstructor를 사용한 생성자 주입

기존에 내가 알고 있던(물론 스프링 공부 시작한지 별로 안됨) 생성자 주입 방식은 이거였다.

 

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

(참고로 memberRepository같은 빈들은 스프링 컨테이너가 관리해준다고 하는데 얘네가 싱글톤 객체라..변하지 않기 때문에 final키워드를 붙이는 게 좋다고 한다)

 

여기서 잠깐 살펴볼 것이, 스프링은 생성자가 1개뿐이라면 @Autowired를 생략해도 된다. 이 경우 @Autowired를 자동으로 인식해 처리하기 때문. 즉 위 코드에서 @Autowired를 빼도 문제가 없다. 

 

그리고 @RequiredArgsConstructor는 final이 붙은 필드들에 대한 생성자를 알아서 만들어준다! 따라서 위 코드를 다음과 같이 아주 편하게 간소화시킬 수 있다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
}

 

원리는 간단하다.

 

  • RequiredArgsConstructor가 생성자를 만들어줌
  • 그 생성자에 대한 Autowired를 자동으로 인식해 처리함

+ Recent posts