개인적인 공부 용으로 정리를 해놓겠다.

 

원래 블로그에 글을 비공개로 올리곤 했었는데, 생각해보니 부족한 글이라도... 혹시 누군가에겐 도움이 되지 않을까 하여,

공개하는 일기 같은 느낌으로... 쓰겠다.

 

 

일단 백엔드에서 JWT Token 을 발급하도록 개발을 해놨다.

스프링 부트로 개발돼있다.

 

 

1. JWT 토큰에 대하여..

JWT 토큰에 대한 건 이 블로그에 잘 정리돼있다.

 

https://velopert.com/2389

 

[JWT] JSON Web Token 소개 및 구조 | VELOPERT.LOG

지난 포스트에서는 토큰 기반 인증 시스템의 기본적인 개념에 대하여 알아보았습니다. 이 포스트를 읽기 전에, 토큰 기반 인증 시스템에 대해서 잘 모르시는 분들은 지난 포스트를 꼭 읽어주세요. 이번 포스트에서는, 토큰 기반 인증 시스템의 구현체인 JWT (JSON Web Token) 에 대한 소개를 하고, JWT 토큰의 기본 구조를 알아보도록 하겠습니다. JSON Web Token 이 뭘까? 기본 정보 JSON Web Token (JWT) 은 웹표준 (RFC

velopert.com

 

 

초 간단 정리

 JWT 는 Json Web Token 의 약자로, 웹표준으로서 두 개체에서 JSON 객체를 사용하여 가볍고 자가수용적인 방법으로 정보를 안정성 있게 전달해준다.

 

웹 표준이고 유명하기 때문에 당연히 여러 프로그래밍 언어에서 지원이 된다.

자가 수용적이란 말은 토큰 자체가 갖고 있는 정보와 그 정보가 인증돼있다는 사실을 갖고 있다는 말이다.

모든 JWT 토큰은 자신이 어떤 토큰인지, 그리고 어떤 정보를 갖고 있는지를 토큰에 정보로 담고 있으며, 이것들을 서명한 값 또한 갖고 있다.

 

보통은 회원 인증 혹은 정보교류에 사용된다. (이건 JWT  토큰만 그런건 아니고)

 

헤더.내용.서명

 

이런 구조로 돼있다.

더보기

 

1. 헤더를 base64 로 인코딩 한다.  (A)  (헤더에는 어떤 방식으로 해싱했는지도 들어간다 (3번에서)

2. 내용을 base64 로 인코딩 한다. (B)

3. A + "." + B 를 "서버 비밀키" 로 해싱하고 base 64 로 인코딩한다. (C)

5. "A.B.C" 를 만들어준다.

 

이제 A.B.C 가 있을때, "A.B" 를 서버가 자신이 가진 비밀키로 해싱 해보면, C 가 나온다는 걸 알기만 하면 된다. (C의 원래 값)

 

이러면 서버는 뭘 알 수가 있는가?

 

아. A.B.C 의 정보가 바뀌지 않았구나! 이걸 알 수 있다.

이걸 알면 뭐가 된다? 그럼 이 토큰은 "조작" 된 건 아니란 걸 알 수 있다.

 

이 토큰은 조작되지 않았고, 이 토큰은 내가 클라이언트에게 준 토큰이니까, 이 토큰을 요청에 함께 보내 놈은 그 클라이언트겠구나. 하는 걸 인증할 수 있는 것이다.

 

 

참고로.. A 와 B 내용 모두 그냥 base64 인코딩이기 때문에... 이 토큰은 말 그대로, 해당 토큰의 대상이 누군지만 명시해야지, 엄청 중요한 정보를 담아 놓으면 안된다.

 

예를 들면 유저의 개인정보 같은 것들을 넣어놓으면, 사실 A,B 를 base64 -> 원래 문자열 로만 바꿔도 그냥 B 본문이 보이기 때문에..

 

핵심은. JWT 토큰은, 서버에서 이 토큰의 내용이 바뀌지 않았구나! 라는 걸 알 수 있을 뿐이다. 

그리고 이 토큰의 A.B 는 누구나 인코딩만 풀면 볼 수 있기 때문에 너무 중요한 데이터를 넣어 놓으면 안된다.

 

JWT 토큰에 조작이 안됐네? 토큰의 B를 보니, 이거 userId 가 "aaa1234" 라고 돼있네? 아 지금 이 토큰을 담은 http 요청은 aaa1234 유저의 요청이구나!  ->이게 다다.

 

 

 

2. 서버 부분은 어떻게 돼있는가?

 

너무 깊게 정리하지는 않아야 한다.

1. 토큰 프로바이더를 만든다. (토큰을 생성하는 놈을 만든다.)

2. 로그인 요청이 오면 낚아 채서, AuthenticationManager 로 인증하고, 해당 유저가 제대로 된 유저다? 싶으면 토큰 프로바이더로 토큰을 만들어 반환한다.

더보기

 

WebSecurityConfigurerAdapter 의 메소드를 오버라이드한 configure 를 보면..

 

@Autowired
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}

 

요렇게 돼있다.

 

내가 만든 userDetailService 는

 

@Service
@Qualifier("custom_user_detail")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
AccountRepository accountRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return accountRepository.findByUsername(username)
.map(t -> new UserDetailsImpl(t))
.orElseThrow(() -> new UsernameNotFoundException(username));
}
}

 

요런 식으로 돼있다. UserDetailsService 인터페이스를 상속한 클래스다. 

하여간 저렇게 AuthenticationManager 가 내가 만든 UserDetailsService 를 사용하게 하고, passwordEncoding 설정도 해서 넣어준다.

 

 

3. JwtAuthenticationFilter 를 만들고 Spring Seucurity 의 Security Filter Chain 의 일원이 되게 해준다.

이 JwtAuthenticationFilter는 내가 만든 JwtTokenProvider 를 사용한다. 요청을 중간에 낚아채서 토큰이 들어있는지 확인하고, 유효한 토큰이면 오케이! 이놈 인증 됐으니까 들여보내!.. 요렇게 된다.

 

더보기
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtTokenProvider tokenProvider;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(httpServletRequest);

            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                Long userId = tokenProvider.getUserIdFromJWT(jwt);

                UserDetailsImpl userDetails = customUserDetailsService.loadUserByUserId(userId);

                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));

                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer "))
            return bearerToken.substring(7);
        return null;
    }
}

 

 

3. React.js 토큰 관리 부분 정리.

 

다른 로그인, 회원가입 부분은 사실상 "서버" 쪽의 역할이 크다. 서버쪽 역할은 뒤로 미뤄두고, 토큰을 어떻게 관리하는지만 정리한다.

 

constnts 의 index.js

일단 상수로 쓰일 변수만 따로 위와 같이 정리해 놓는다.

 

options 부분을 살펴보자.

 

options = Object.assign({}, defaults, options);

 

header 설정을 Authorization, Bearer ~~ 로 해준다.

그리고 이 defaults 헤더와 추가로 주어진 옵션을 합쳐서 fetch library 를 사용해 통신한다.

 

fetch library 는 react 에서 기본적으로 제공되는 라이브러리이다. 이 부분은 리액트 프론트 부분을 다시 정리하면서, axios 로 바꿀 것이다.

 

localStorage를 사용한다.

 

 

localStorage에 대한 정리는 이쪽 글에 잘 정리돼 있다.

 

https://www.zerocho.com/category/HTML&DOM/post/5918515b1ed39f00182d3048

 

 

더보기

 로컬 스토리지(localStorage) 세션 스토리지(sessionStorage)에 대해 알아보겠습니다. 이름만 봐도 각각의 기능이 뭔지 대충 알겠죠? 영어에 약하신 분들을 위해 간단히 설명드리자면, 로컬 스토리지와 세션 스토리지는 HTML5에서 추가된 저장소입니다. 간단한 키와 값을 저장할 수 있습니다. 키-밸류 스토리지의 형태입니다.

 

로컬 스토리지와 세션 스토리지의 차이점은 데이터의 영구성입니다. 로컬 스토리지의 데이터는 사용자가 지우지 않는 이상 계속 브라우저에 남아 있습니다. 하지만 세션 스토리지의 데이터는 윈도우나 브라우저 탭을 닫을 경우 제거됩니다. 지속적으로 필요한 데이터(자동 로그인 등)는 로컬 스토리지에 저장하고, 잠깐 동안 필요한 정보(일회성 로그인 정보라든가)는 세션 스토리지에 저장하면 되겠습니다. 하지만 비밀번호같은 중요한 정보는 절대 저장하지 마세요! 클라이언트에 저장하는 것이기 때문에 언제든지 털릴 수 있습니다.

 

 두 스토리지는 모두 window 객체 안에 들어 있습니다. Storage 객체를 상속받기 때문에 메소드가 공통적으로 존재합니다. 도메인 별 용량 제한도 있습니다.(프로토콜, 호스트, 포트가 같으면 같은 스토리지를 공유합니다) 브라우저별로, 기기별로 다르긴 하지만 모바일은 2.5mb, 데스크탑은 5mb~10mb라고 생각하시면 됩니다. 

 

 

메소드를 간단히 설명하자면, localStorage.setItem(, )으로 로컬스토리지에 저장한 후, localStorage.getItem()로 조회하면 됩니다. localStorage.removeItem()하면 해당 키가 지워지고, localStorage.clear()하면 스토리지 전체가 비워집니다.

 

객체는 제대로 저장되지 않고 toString 메소드가 호출된 형태로 저장됩니다. [object 생성자]형으로 저장되는 거죠. 객체를 저장하려면 두 가지 방법이 있습니다. 그냥 키-값 형식으로 풀어서 여러 개를 저장할 수도 있습니다. 한 번에 한 객체를 통째로 저장하려면 JSON.stringify를 해야됩니다. 객체 형식 그대로 문자열로 변환하는 거죠. 받을 때는 JSON.parse하면 됩니다.

 

세션 스토리지는 window.sessionStorage에 위치합니다. clear, getItem, setItem, removeItem, key 등 모든 메소드가 같습니다. 단지 로컬스토리지와는 다르게 데이터가 영구적으로 보관되지는 않을 뿐입니다.

------

 

 

 매 서버 요청마다 서버로 쿠키가 같이 전송됩니다. 왜 서버에 쿠키가 전송될까요?

그것을 알기 위해서는 HTTP 요청에 대해 먼저 간단히 알아야 합니다. HTTP 요청은 상태를 가지고 있지 않습니다. 무슨 말인가 하면, 브라우저에서 서버로 나에 대한 정보를 가져오라는 GET /me라는 요청을 보낼 때, 서버는 요청 자체만으로는 그 요청이 누구에게서 오는 지 알 수 없습니다. 그래서 응답을 보낼 수가 없죠. 이 때 쿠키에 나에 대한 정보를 담아서 서버로 보내면 서버는 쿠키를 읽어서 내가 누군지 파악합니다. 쿠키는 처음부터 서버와 클라이언트 간의 지속적인 데이터 교환을 위해 만들어졌기 때문에 서버로 계속 전송되는 겁니다.

----

 

 

 

 

  앱의 가장 상단. App.js 에서 토큰이 있으면 로그인 돼있는지 확인하고, 토큰이 없으면 로그인 되지 않았다고 판단한다.

 

export function getCurrentUser() {
  if (!localStorage.getItem(ACCESS_TOKEN)) {
    return Promise.reject("No access token set.");
  }

  return request({
    url: API_BASE_URL + "/user/me",
    method: "GET"
  });
}

 

위는 해당 행위를 하는 함수다.

 

fetch 는 기본적으로 Promise 기반이다. 위와 같이, 토큰이 없을경우 바로 Promise.reject(); 해준다.

토큰이 있으면 유저 정보를 확인한다.

 

loadCurrentUser() {
    this.setState({
      isLoading: true
    });

    getCurrentUser()
      .then(response => {
        this.setState({
          currentUser: response,
          isAuthenticated: true,
          isLoading: false
        });
        console.log(this.state);
      })
      .catch(error => {
        console.log(error);
        this.setState({
          isLoading: false
        });
      });
  }

  componentDidMount() {
    this.loadCurrentUser();
  }

 

getCurrentUser() 에서 반환하는 Promise 에 대하여 위와 같이 작성을 해준다.

 

componentDidMount() 생성주기 콜백을 보면, mount 되면 바로 loadCurrentUser를 실행하는 것을 볼 수 있다.

 

 

결국 간단히 정리하자면

 

1. 앱의 상단에서 토큰이 있는지 확인하고, 있다면 유효한 토큰인지 확인해서 유저 정보를 가져오고, 토큰이 없으면 그냥 로그인 안된 것으로 간주한다.

2. 로그인 버튼을 클릭해서, 로그인 화면으로 넘어가고... 아이디 비밀번호를 입력해서 로그인 하면, 토큰을 받고. 토큰을 받으면 그걸 localStorage에 저장해주고... 로그인 됐다는 것을 처리 해준다.

 

  handleLogin() {
    console.log("login Success");

    this.loadCurrentUser();
    this.props.history.push("/");
  }

로그인이 성공했을 때 실행되는 함수는 위와 같이 해놨다. 일단 localStorag에 토큰을 저장해놓고, handleLogin() 함수를 실행하면, loadCurrentUser를 재실행 하면서, localStorage에 있는 토큰으로 다시 한번 토큰이 유효한지 확인 후 유저 정보를 가져온다.

 

 

블로그 이미지

맛간망고소바

,

나를 위한 정리이다.

 

1. DataSource 설정을 해준다. 

 

https://minwan1.github.io/2017/04/08/2017-04-08-Datasource,JdbcTemplate/

 

Wan Blog

WanBlog | 개발블로그

minwan1.github.io

위의 블로그에 잘 정리돼있는 개념을 살짝 정리하자.

 

더보기

JDBC

JDBC는 Java Database Connectivity의 약어로 자바 언어로 다양한 종류의 관계형 데이터베이스에 접속하고 SQL문을 수행하여 처리하고자할때 사용되는 표준 SQL 인터페이스 API이다.

 

 

Connection Pool

Connection Pool 이란 클라이언트의 요청 시점에 Connection을 연결하는 것이 아니라 미리 일정수의 Connection 을 만들어 놓고 필요한 어플리케이션에 전달하여 이용하도록 하는 방법이다.

 

Connection

DriverManager.getConnection()은 실제 자바프로그램과 데이터베이스를 네트워크상에서 연결을 해주는 메소드이다. 연결에 성공하면 DB와 연결 상태를 Connection 객체로 표현하여 반환한다. 보통 Connection하나당 트랜잭션 하나를 관리한다. 트랜잭션은 하나 이상의 쿼리에서 동일한 Connection 객체를 공유하는 것을 뜻한다. Mybatis의 SqlSession, Hibernate에 TransactionManager등의 Close가 이루어지면 Connection을 ConnectionPool에 반납하게 된다.

 

DataSource

javax.sql.DataSource 는 인터페이스는 ConnectionPool을 관리하는 목적으로 사용되는 객체로 Application에서는 이 Datasource 인터페이스를 통해서 Connection을 얻어오고 반납하는 등의 작업을 구현해야하는 인터페이스이다. Connection Pool에는 여러개의 Connection 객체가 생성되어 운용되어진다. 각각을 직접 웹 어플리케이션에서 이용하면 체계적인 관리가 힘들게 되므로 DataSource라는 개념을 도입하여 사용하고 있다. DataSource 인터페이스는 Connection pool을 어플리케이션단에서 어떻게 관리할지를 구현해야하는 인터페이스이다.

 

JdbcTemplate

Statement 처리를 위해서 Spring에서 JDBC를 지원하는데 그것이 JdbcTemplate 클래스, JdbcTemplate 클래스는 setDataSource()가 있어서 생성자 방식으로 Datasource를 지정가능하다. 아래는 Mybatis를이용한 간단한 JdbcTemplate예제이다.

 

 

 

JDBC는 자바에서 기본적으로 명시해 놓은, 데이터베이스 연결 및, 처리를 위한 표준 SQL *인터페이스 API야.

DataSource는 Connection Pool 을 관리하기 위해 쓰이는 *인터페이스야.

 

자바에서 기본적으로 SQL 을 다루기 위해서는 JDBC를 사용해.

그리고, 이 JDBC 를 사용하면서 (물론 Database를 이용하면 어쩔 수 없지만) Connection 이 생겨. 이 Connection 을 미리 생성해 놓는 것이 Connection Pool 이고, 이 Connection Pool 을 관리하기 위한 인터페이스는 DataSource인거지.

 

여기까지는 자바의 기본 스펙이야. 스프링만의 것이 아니지.

 

그런데 스프링은 이런 DataSource 설정을 아주 쉽게 할 수 있게 해줬지.

 

하여간. 애플리케이션을 만드는 첫 번째 스텝은 DataSource 설정이야.

 

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("datasource.config")
    public DataSource dataSource() {
         return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
}

application.yml 설정은 대충 다음과 같음

spring:
  jpa:
    hibernate:
      ddl-auto: update
    generate-ddl: true
    show-sql: true
    database-platform: "org.hibernate.dialect.MySQL5Dialect"
  jackson:
    time-zone: UTC
  datasource:
    initialization-mode: always
app:
  jwtSecret: [JWT Secret Key]
  jwtExpirationInMs: 604800000
datasource:
  config:
    jdbc-url: "jdbc:mysql://localhost:3306/[DB이름]"
    username: "아이디"
    password: "비밀번호"
    maximum-pool-size: "4"

 

2. UserDetails 를 반환할 수 있도록 하자

 

웬만한 웹 어플리케이션에는 계정이 있어야겠지. Spring Security 를 위한 인터페이스를 구현해야해.

 

@Data
@Entity
@Table(name = "accounts", uniqueConstraints = {
        @UniqueConstraint(columnNames = {"username"}),
        @UniqueConstraint(columnNames = {"email"})
})
@NoArgsConstructor
public class Account extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(max = 40)
    private String name;

    @NotBlank
    @Size(max = 15)
    private String username;

    @NotBlank
    private String password;

    @NaturalId
    @NotBlank
    private String email;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "account_roles", joinColumns = @JoinColumn(name = "account_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles = new HashSet<>();


    public Account(String name, String username, String password, String email) {
        this.name = name;
        this.username = username;
        this.password = password;
        this.email = email;
    }
}

 

상속한 DateAudit 은 일단 무시하자. 위는 백기선 님의 Youtube 영상을 보며 따라 쓴 Account야. 사실 이렇게 안만들어도돼. 왜냐하면 중요한건 따로 있거든.

 

@Entity
@Data
@Table(name="roles")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    @NaturalId
    @Column(length=60)
    private RoleName name;

}

 

Role 은 대충 위와 같이 만들자.

 

@Service
@Qualifier("custom_user_detail")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return accountRepository.findByUsername(username)
                .map(t -> new UserDetailsImpl(t))
                .orElseThrow(() -> new UsernameNotFoundException(username));
    }
}

 

중요한건 위와 같은 서비스인데. 정확히는 UserDetails를 반환하게 해주는 서비스야.

 

@Data
public class UserDetailsImpl extends User {
    private Long id;

    public UserDetailsImpl(Account account) {
        super(account.getUsername(), account.getPassword(), authorities(account));
        this.id = account.getId();
    }

    private static Collection<? extends GrantedAuthority> authorities(Account account) {
        return account.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                .collect(Collectors.toList());
    }
}

 

위를 보면 알 수 있다싶이, UserDetailsImpl 은 User을 상속하고 있어.

위의 User 클래스는 springframework.security.core.userdetails 에 있는 클래스야. 저 클래스가 애초에 UserDetails 를 상속하고 있지.

UserDetails ->(상속) User ->(상속) UserDetailsImpl(내가 만듦)

 

그리고 UserDetailsImpl 클래스는 Account Entity에 의존하고 있지.

 

이렇게 하면, UserDetailsImpl 의 생성자를 보면,

super() 안에

username, password, Collection<? extends GrantedAuthority> 를 넘기고 있어.

위를 보면 알다시피, SimpleGrantedAuthority 로 account의 roles 를 mapping 하고 있지.

 

정리

Spring Security를 사용할 수 있도록,

UserDetails 를 반환할 수 있게 서비스와 클래스를 구현하자.

 

 

3. Spring Security 설정

 

UserDetails 를 반환할 수 있도록 했으면, (물론 가입 및 로그인을 위한 컨트롤러 같은 것들도 만들어야 하지만.)

이제 계정을 이용해 인증을 진행할 수 있도록 해야 한다.

 

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("custom_user_detail")
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtAuthenticationEntryPoint unAuthorizedHandler;

    @Autowired
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

    @Override
    public void configure(WebSecurity webSecurity) {
        webSecurity.ignoring().antMatchers("/res/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/account/**").access("ROLE_USER")
                .antMatchers("/admin/**").access("ROLE_ADMIN")
                .antMatchers("/", "/login", "/login-error", "/encrypt_password", "/api/auth/signin", "/api/auth/signup").permitAll()
                .antMatchers("/**").authenticated();

        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(unAuthorizedHandler)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.formLogin()
                .loginPage("http://localhost:3000");

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

위는 짜집기하면서 더러워진 설정이다.

 

위에서 주목해야 할 것은.

 

1. configure override(1)

 

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

userDetailService -> userDetails 반환

passowordEncoder -> 패스워드 암호화 및 복호화 담당.

 

2. configure override(2)

 

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/account/**").access("ROLE_USER")
                .antMatchers("/admin/**").access("ROLE_ADMIN")
                .antMatchers("/", "/login", "/login-error", "/encrypt_password", "/api/auth/signin", "/api/auth/signup").permitAll()
                .antMatchers("/**").authenticated();

        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(unAuthorizedHandler)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.formLogin()
                .loginPage("http://localhost:3000");

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

 

JwtAuthenticationFilter 를 Bean 으로 만들어서 Filter 에 포함시킨다.

addFilterBefore (마지막줄) 을 잘 보자.

 

위 설정은 지금 정리돼지 않은 쓰레기이다. 왜냐면... 처음에는 session 방식으로 구현을 하다가, 중간에 jwt token 을 이용한 방식으로 바꿨기 때문이다. + spa 프론트를 사용했다. 따라서 프론트에서 알아서 토큰이 없으면 로그인 페이지로 이동시킨다.

 

따라소 http.formLogin().loginPage(~~~ 요 부분이 필요가 없다.

sessionCreatePolicy -> STATELESS 다. (jwtToken 을 사용하기 때문)

 

 

JWT 는 애초에 서버를 Stateless 하게 만드려는 목적이 있긴 한데... session 관리가 생각보다 복잡하다.

 

일단

 

1. DataSource 설정

2. UserDetails 설정

3. 인증 설정

 

여기까지 하면 기본적인 시작점 까지는 왔다고 생각한다.

블로그 이미지

맛간망고소바

,

해당 내용은 

그림으로 공부하는 IT 인프라 구조를 기반으로 정리하였습니다.

 

1. 배열과 연결 리스트

 

배열과 연결리스트는 모두 순차적으로 데이터를 처리하는 구조이지만, 구조가 다르기에 성능 측면의 특징도 많이 다르다.

배열과 연결리스트는 다양한 데이터구조를 이해하기 위한 기본 구조이다.

 

배열은 같은형태의 상자를 빈틈없이 순서대로 나열한 데이터 구조다. "같은크기" 의 상자를 "빈틈없이 순서대로" 나열하기 때문에 몇 번째 상자인지만 알면 해당 상자에 한번에 액세스 할 수 있다.

 

 연결 리스트는 상자를 선으로 연결한 형태의 데이터 구조다. 다음 상자의 위치 정보를 가지고 있다. 빈틈 없이 나열되지 않아도 되는 대신, 상자를 찾으려면 끝에서부터 순서대로 하나씩 상자 내부를 확인해야 한다.

 

즉 배열이 탐색은 빠르다. 하지만 배열은 도중에 상자를 추가하려면 그 이후 상자를 전부 하나씩 뒤로 옮겨야 한다. 상자를 빼는 경우엔 모두 하나씩 앞으로 옮겨야 한다. 이런 이유로 배열은 데이터 추가 및 삭제가 느린 데이터 구조라고 할 수 있다.

 

반면 연결 리스트는 도중에 상자를 추가하거나 삭제하면 선만 바꿔서 연결 해주면 된다. 따라서 연결 리스트는 데이터 추가, 삭제가 빠른 데이터 구조라 할 수 있다.

 

 특징을 간단히 정리하면 다음과 같다.

 

  •  배열은 데이터를 빈틈없이 순서대로 나열한 데이터 구조
  • 연결 리스트는 데이터를 선으로 연결한 데이터 구조
  • 탐색이 빠른 것은 배열이고, 느린 것은 연결 리스트
  • 데이터 추가, 삭제가 빠른 것은 연결 리스트, 느린 것은 배열.

 

참고로 위의 탐색은 처음부터 연속으로 찾는 것을 의미하는 것이 아니라, N 번째 요소 탐색을 의미한다. 

 

위의 삭제가 빠른 연결 리스트와 탐색이 빠른 배열을 조합한 하이브리드형 데이터 구조가"해시 테이블" 이다.

 

 2. 해시 테이블 / 트리, 인덱스

 

간단히 얘기하자면, modular 연산을 통해, 배열의 어느 인덱스에 해당하는 놈인지 찾고.

이 배열에는 각각 연결 리스트가 있어서, 배열에 연결된 연결 리스트 안의 데이터를 순차적으로 탐색해서 똑같은 놈이 있는지 찾아낸다.

 

해시 테이블은 키와 값 조합으로 표를 구성한 데이터 구성이다. 키는 해시 함수를 통해 해시 값으로 변환된다. 해시 값은 고정길이 데이터이기 때문에 조합 표의 데이터 구조가 간단해서 검색이 빠르다는 장점이 있다. 해시 테이블에서는 아무리 데이터 양이 많아진다고 해도 기본적인 등호 검색의 속도는 변하지 않는다. 

 검색 속도는 1이나 다름 없으나, 범위 검색이 약하다는 문제가 있다.

 

데이터베이스에서 인덱스를 사용하면 왜 검색이 빨라지는 걸까?

인덱스를 사용한다고 해서 항상 빨라지는 것이 아닌 이유는 왜일까?

기존 DBMS와 인메모리 DB에 적합한 인덱스가 다른 이유는 무엇일까?

 

 해시나 트리는 탐색 알고리즘이 아닌 데이터 구조이지만, 효율적인 탐색을 위해 사용된다. 필요한 때에 필요한 데이터를 신속하게 찾기 위해서는 데이터를 정리해둘 필요가 있다.

 

  • 필요한 때에 필요한 데이터를 빠르게 찾기 위해서 데이터를 정리해 둘 필요가 있다.
  • 데이터를 찾을 때의 데이터 구조와 데이터 저장 방식 특성에 따라 적합한 데이터 "정리 방법" 이 달라진다.
  • 데이터의 정리 방법을 데이터 구조, 찾는 방법을 탐색 알고리즘이라고 한다.
  • 처리 상대에 맞추어 데이터 구조를 정리할 필요가 있기 때문에 알고리즘과 데이터 구조는 자주 함깨 다루어진다.

 

 

 인덱스 스캔을 한다고 반드시 빨라지는 것은 아니다. 디스크에서 원하는 데이터를 얼마나 적은 노력으로 추출하느냐가 관건으로, 인덱스는 이를 위한 하나의 수단에 불과하다. 

 

 인덱스가 없는 경우에는, SQL을 발행해서 한 건의 데이터를 취득하는 경우라도 디스크에서 테이블 데이터를 모두 읽어서 조사해야 한다. 테이블의 모든 블록을 처음부터 순서대로 읽어나가는 것을 풀 스캔(full scan) 이라고 한다. 색인도 없는 사전에서 단어를 찾는 것과 같다.

 

 DBMS 인덱스로 일반적으로 사용되는 것은 B트리 인덱스다.

 

인덱스가 있으면 최소한의 필요 블록만 읽으면 된다. '인덱스'는 우리말로 '색인'이다. 사전을 찾을 때 색인을 이용하는 것과 마찬가지다.

 

 하지만 인덱스가 있다고 무조건 좋은 것만은 아니다. 검색이 빨라지는 대신에 데이터 추가, 갱식, 삭제 시에 테이블 뿐마 아니라 인덱스 데이터도 갱신해야 한다. 인덱스 갱신 때문에 불필요한 오버헤드가 발생할 수 있다. 

 

인덱스는 읽을 블록수를 줄이기 위한 수단이지만, 인덱스를 사용하면 오히려 읽을 블록수가 늘어날 수도 있다. 예를들어 테이블 데이터를 모두 취득해야 하는 경우 등이다. 이때는 테이블의 모든 블록뿐만 아니라 인덱스 블록까지 읽어야 하기 때문에 디스크 I/O 가 증가한다. 

 일반적으로 DBMS는 풀스캔을 하는 경우, 1회 디스크 I/O로 가능한 한 큰 크기의 데이터를 일겅서 I/O 횟수를 줄이려 한다. 하지만 인덱스 스캔에서는 인덱스 블록을 읽으면서 테이블 블록을 하나씩 읽기 때문에 액세스 블록 수가 늘어남과 동시에 I/O 횟수가 늘어난다.

 

 B트리가 자주 사용되는 것은, 트리 구조 계층이 깊어지지 않도록 디스크 I/O를 최소한으로 제어하기 때문이다. 반대로 인메모리 DB에서는 디스크 I/O를 신경 쓸 필요가 없기 때문에 T 트리 인덱스라는, 이진 트리의 일종을 사용하는 경우가 있다. 이진 트리는 가지가 두개 밖에 없어서 계층이 깊어지지만, 키 값 비교 횟수가 적다는 이점이 있다

 

더보기

 B트리로 이루어진 인덱스는, 루트 블록, 브랜치 블록, 리프 블록으로 분류된다.

잎 부분이 원하는 데이터 자장위치가 기록된 곳이다.

 

 

 

 데이터 구조는 데이터를 찾는 방식이나 데이터 저장 위치의 특성을 고려해서 선택할 필요가 있다. 또한, DBMS에서 인덱스를 만들면 검색은 빨라지지만 갱신 시에 오버헤드가 걸린다는 단점도 있기 때문에 함께 고려해야 한다.

블로그 이미지

맛간망고소바

,

엄청난 요약본이다.

토비의 스프링이 너무 정리가 잘 돼있어서.... 음...

결국 받아쓰기가 된 것 같은 느낌이 ㅠㅠ

 

AOP

 

 이제 AOP 에 대해서 알아보자. AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반기술의 하나다. AOP를 바르게 하려면 AOP의 필연적인 등장 배경과 스프링이 도입한 이유, 장점을 제대로 이해해야 한다. 스프링에 적용된 가장 인기 있는 AOP의 적용 대상은 바로 "선언적 트랜잭션 기능" 이다. 

 

제대로 들어가기전에 다이내픽 프록시와 팩토리 빈에 대해서 알고 넘어가자.

 

 단순히 확장성을 고려해서 한 가지 기능을 분리한다면 전형적인 전략 패턴으르 사용하면 된다. 하지만 우리가 원하는건 트랜잭션을 적용한다는 사실 자체가, 우리가 짠 코드에는 보이지 않는 것이다. (메소드 안에 안 드러나게)

 

 트랜잭션이라는 기능은 사용자 관리 비즈니스 로직과는 성격이 다르기 때문에 아예 그 적용 사실 자체를 밖으로 분리할 수 있다.

 

 핵심기능은 부가기능을 가진 클래스의 존재 자체를 모른다. 그래서 부가기능은 마치 자신이 핵심기능을 가진 클래스인것처럼 꾸며서, 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 한다. 

 

 이렇게 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프록시(proxy) 라고 부른다. 

 

클라이언트 -> 프록시 -> 타깃

 

프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있는 위치에 있다는 것이다.

 

데코레이터 패턴.

 

 간단히 설명하자면, 프록시 개수를 확정하지 않는, 프록시 개념을 이용하는 프록시 비스무레한 패턴이다. (물론 둘은 다르다.)

InputStream 이라는 인터페이스를 구현한 타깃인 FileInputStream에 버퍼 읽기 기능을 제공해주는 BufferedInputStream 이라는 데코레이터를 사용하는 예시를 보자.

 

InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));

 

프록시로 동작하는 각 데코레이터는 위임하는 대상에도 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지, 아니면 다음 단계의 데코레이터 프록시로 위임하는지 알지 못한다.

 

프록시 패턴.

 

일반적으로 사용하는 프록시라는 용어와 디자인 패턴에서 말하는 프록시 패턴은 구분할 필요가 있다. 전자는 클라이언트와 사용 대상 사이에 대리 역할을 맡은 오브젝트를 두는 방법을 총칭한다면,

 

 후자는 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우를 가리킨다.

 

프록시 패턴의 활용 예 중 하나는, 타깃 오브젝트를 미리 만들지 않고, 사용하는 시점에 만들고 싶다. 하지만 타깃 오브젝트에 대한 레퍼런스가 미리 필요하다. 요런 상황에, 프록시 패턴을 적용해서 실제 타깃 오브젝트 대신 프록시를 넘겨준다. 프록시의 메소드를 통해 타깃을 사용하려고 하면, 그 때 프록시가 타깃 오브젝ㅌ르르 생성하고 요청을 위임해준다.

 

 또는 원격 프로젝트를 이용하는 경우... 다른 서버에 존재하는 오브젝트를 사용해야 한다면, 원격 오브젝트에 대한 프록시를 만들어주고 클라이언트는 마치 로컬에 존재하는 오브젝트를 쓰는 것처럼 프록시를 사용하게 할 수 있다.

 

 또튼 타깃에 대한 접근권한을 제어하기 위해 프록시 패턴을 사용할 수 있다. 프록시의 특정 메소드를 사용하려고 하면 접근이 불가능 하다고 예외를 던진다.

 Collections의 unmodifiableCollection() 을 통해 만들어진 오브젝트가 전형적인 접근권한 제어용 프록시라 볼 수 있다.

 

 프록시와 데코레이터는 상당히 유사하지만, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고있는 경우가 많다. 

 

 

 

 다이내믹 프록시

 

프록시를 일일이 만드는 것은 귀찬핟. 인터페으스를 구현해서 클래스를 새로 정의하지 않고 프록시를 편리하게 사용할 방법은 없을까? 있다. 자바에서는 java.lang.reflect 패키지 않에 프록시를 손쉽게 만들 수 있도록 지원해주는 클래스들이 있다. 일일이 프록시 클래스를 정의하지 않고도 몇 가지 API를 이용해 프록시처럼 동작하는 오브젝트를 다이내믹하게 생성하는 것이다.

 

 프록시는 다음의 두 가지 기능으로 구성된다.

 - 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출도면 타깃 오브젝트로 위임한다.

 - 지정된 요청에 대해서는 부가기능을 수행한다.

 

 

 리플렉션

 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다. 

 자바의 모든 클래슨느 그 클래스 자체의 구성정보를 담은 Class 타입의 오브젝트를 하나씩 갖고 있다. 클래스이름.class 라고 하거나 오브젝트의 getClass() 메소드를 호출하면 클래스 정보를 담은 Class 타입의 오브젝트를 가져올 수 있다.

 

 

리플렉션 API 메소드 중에서 메소드에 대한 정의를 담은 Method라는 인터페이스를 이용해 메소드 호출을 해보자.

 

Method 인터페이스에 정의된 invoke() 메소드를 사용하자.

 

 public Object invoke(Object obj, Object... args);

 

Method lengthMethod = String.class.getMethod("length");

int length = lengthMethod.invoke(name);

 

 

 

 다이내믹 프록시는 "프록시 팩토리" 에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트다. 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다.

 

 클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다. 이 덕분에 프록시를 만들 때 인터페이스를 모두 구현해가면서 클래스를 정의하는 수고를 덜 수 있다. 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어주기 때문이다.

 

 다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만, 프록시로서 필요한 부가긴으 제공 코드는 직접 작성해야 한다. 부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담는다. InvocationHandler 인터페이스는 다음과 같은 메소드 한 개만 가진 간단한 인터페이스다.

 

에라이...

 

 

간단히 InvocationHandler 의 구현 클래스를 정리하고 넘어가자...

 

public class UppercaseHandler implements InvocationHandler {

 Hello target;

 

 public UppercaseHandler(Hello target) {

  this.target=target;

 }

 public Object invoke(Object proxy, Method method, Object[] args) {

  String ret = (String) method.invoke(target,args);

 return ret.toUpperCase();

 }

}

 

다이내믹 프록시의 생성은 Proxy클래스의 newProxyInstance() 스태틱 팩토리 메소드를 이용하면 된다.

 

Proxy.newProxyInstance( getClass().getClassLoader(), newClass[] {Hello.calss}, new UppercaseHandler(new HelloTarget()));

 

....

첫 번째 파라미터는 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더,

두번 째는 구현할 인터페이스,

세 번재는 부가기능과 위임 코드를 담은 Invocationhandler이다.

 

 

Class의 이름을 알고 있다면 Class.forName("java.util.Date").newInstance() 로 기본 생성자를 호출해서 값일 받아올 수 있다.

리플렉션 API이다.

 

 

 

팩토리 빈

 

 팩토리빈이란 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말한다.

 

 

다이내믹 프록시를 만들어주는 팩토리 빈

 

 Proxy의 newProxyInstance() 메소드를 통해서만 생성이 가능한 다이내믹 프록시 오브젝트는 일반적인 방법으로는 스프링의 빈으로 등록할 수 없다. 대신 팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어줄 수가 있다.

 

 

 다이내믹 프록시를 생성해주는 팩토리 빈을 사용하는 방법은 여러 가지 장점이 있다. 한 번 부가기능을 가진 프록시를 생성하는 팩토리 빈을 만들어두면 타깃의 타입에 상관없이 재사용 할 수 있기 때문이다.

 

 이렇게 프록시 팩토리 빈을 사용하면 1. 인터페이스를 구현하는 프록시 클래스를 일일이 만들 필요가 없어짐. 2. 부가적인 기능이 여러 메소드에 반복적으로 나타나게 되는 코드 중복이 없어짐.

 

 하지만 단점이 있다.

 1. 비슷한 프록시 팩토리 빈의 설정이 중복되는건 막을 수 없음. (트랜잭션과 같이 비즈니스 로직을 담은 클래스의 메소드에 적용한다면...)

2. 하나의 타갯에 여러개의 부가기능을 넣는것도 애매함.

3. 프록시 팩토리 빈 개수만큼 InvocationHandler 오브젝트가 생김.

 

 

 

스프링의 프록시 팩토리 빈

 

이제 스프링의 프록시 팩토리 빈을 살펴보자.

 

스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다.

 

생성된 프록시는 스프링의 빈으로 등록돼야 한다. 스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 "팩토리 빈"을 제공해준다.

 

 여기는 InvoacationHandler가 아니라, MethodInterceptor 인퍼에스를 구현해서 부가기능을 만든다.

InvocationHandler의 invoke() 메소드는 타깃 오브젝트에 대한 정보를 제공하지 않는다, 따라서 타깃은 InvocationHandler를 구현한 클래스가 직접 알고 있어야 한다. 반면 MethodInterceoptor의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브ㅔㄱ트에 대한 정보도 함께 제공 받는다. 그 차이 덕분에 MethodInterceptor는 타깃 오브젝트에 상관 없이 독립적으로 만들어질 수 있다. 따라서 여러 프록시에서 함께 사용할 수 있고, 싱글톤 핀으로 등록 가능하다.

 

 

 어드바이스: 타깃이 필요없는 순수한 부가기능.

 

 MethodInterceptor 처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는 어드바이스(advice) 라고 부른다. 어드바이스는 타깃 오브젝트에 "종속 되지 않는" 순수한 부가기능을 담은 오브젝트다.

 

 포인트컷: 부가기능이 적용될 메소드 선정 알고리즘을 담은 오브젝트. 

 

 MethodInterceptor 오브젝트는 여러 프록시가 공유해서 사용할 수 있다. 따라서 타깃 정보를 갖고 있지 않다. 그 덕분에 MethodInterceptor 는 스프링의 싱글톤 빈으로 등록할 수 있다. 

 그렇다면 타깃에 대한 벙보가 없는 MethodInterceptor는 어떻게 부가기능을 적용할 메소드를 "선별" 할 수 있을까?

스프링은 메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다.

 

 

 

 

 

스프링의 AOP

 

위와 뭐가 다르냐? 방금전 위는 스프링의 프록시 팩토리 빈이었다.

 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제는 스프링의 ProxyFactoryBean의 어드바이스를 통해 해결됐다.

남은 것은 부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정 정보를 추가해주는 부분이다.

 

 반복적인 프록시의 메소드 구현을, 코드 자동생성 기법을 이용해 해결했다. -> 반복적인 ProxyFactoryBean 설정 문제는 설정 자동방법 기법으로 해결하면 안되나? -> 또는 실제 빈 오브젝트가 되는 것은 ProxyFactoryBean을 통해 생성되는 프록시 그 자체이니까 프록시가 자동으로 빈으로 생성되게 할 수는 없을까?

 

빈 후처리기를 이용한 자동 프록시 생성기

 

  스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트를 제공해준다.

 

 그중에서 관심을 가질 만한 확장 포인트는 BeanPostProcessor 인터페이스를 구현해서 만드는 "빈 후 처리기" 이다.

 빈 후처리기는 말 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해준다.

 

 여기서는 스프링이 제공하는 빈 후처리기 중의 하나인 DefaultAdvisorAutoProxyCreator 를 살펴보겠다. 이름을 보면 알 수 있듯이 DefaultAdvisorAutoProxyCreator는 어드바이저를 이용한 자동 프록시 생성기다.

 

 스프링은 빈 후처리기가 빈에 등록돼 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.

빈 오브젝트의 프로퍼티를 강제로 수정할 수도 있고, 별도의 초기화 작업을 수행할 수도 있다. 심지어는 만들어진 빈 오브젝트를 바꿔칠수도 있다. 따라서 스프링으 설정을 참고해서 만든 오브젝트가 아닌 다른 오브젝트를 빈으로 등록시키는 것이 가능하다..!

 

 -> 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수 있다!

 

확장된 포인트컷

 사실 포인트컷은 메소드만 선별할 수 있는게 아니라, 클래스도 선별 가능하다. DefaultAdivisorAutoProxyCreator는 클래스와 메소드 선정 아록리즘을 모두 갖고 있는 포인트컷이 필요하다. 정화깋는 그런 포인트컷과 어드바이스가 결합되어 있는 "어드바이저" 가 등록돼 있어야 한다.

 

 

 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator는 등록된 빈 중에서 Advisor 인터페이스를 구현한 것을 모두 찾는다. 그리고 생성되는 모든 빈에 대해 어드바이저의 포인트컷을 적용해보면서 프록시 적용 대상을 선정한다. 빈 클래스가 프록시의 선정 대상이라면 프록시를 만들어 원래 빈 오브젝트와 바꿔치기한다.

 

 

 

AOP: 애스펙트 지향 프로그래밍

 

전통적인 객체지향 기술의 설계 방법으로는 독립적인 모듈화가 불가능한 트랜잭션 경계설정과 같은 부가기능을 어떻게 모듈화 할 것인가 연구해온 사람들은, 이 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했다.

 

 그래서 이런 부가기능 모듈을 "애스펙트" 라고 부르기 시작했다.

애스펙트란 그 자체로 애플리케이션의 핵심기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.

 

애스펙트는 그 단어의 의미대로 애플리케이션을 구성하는 한 가지의 측면이라고 생각할 수 있다.

 

 이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을 애스펙트 지향 프로그래밍 또는 약자로 AOP 라고 부른다. AOP는 OOP를 돕는 보조적인 기술이지 OOP를 완전히 대체하는 새로운 개념은 아니다.

 

 AOP는 결국 애플리케이션을 다양한 측면에서 독립적으로 모델링하고, 설계하고, 개발할 수 있도록 만들어주는 것이다. 애플리케이션을 핵심 로직 대신 어느 부가기능의 관점에서 바라보고, 그 부분에 집중해서 설계하고 개발할 수 있게 해주는 것. 이렇게 특정한 관점을 기준으로 바라볼 수 있게 해준다는 의미에서  AOP를 관점지향 프로그래밍이라고 하는 것이다.

 

블로그 이미지

맛간망고소바

,

일단 기본적인 JDBC, DataSource, Connection Pool 등에 대한 정리는

 

https://minwan1.github.io/2017/04/08/2017-04-08-Datasource,JdbcTemplate/

 

Wan Blog

WanBlog | 개발블로그

minwan1.github.io

이곳에 무지 잘 설명돼 있으니, 여기를 살펴보자.

 

 

 트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다. 전체가 다 성공하든지 아니면 전체가 다 실패해야 한다. 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다. 이것이 트랜잭션이다.

 DB는 그 자체로 트랜잭션을 지원한다. 하나의 SQL 명령을 처리하는 경우는 DB가 트랜잭션을 보장해준다고 믿을 수 있다. 하지만 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야 하는 경우가 있다. 중간에 실패가 발생해서 그 전에 처리한 SQL 작업까지 모두 취소하는 작업을 트랜잭션 롤백이라고 한다.

 반대로 SQL이 모두 성공적으로 마무리 됐으면 DB에 알려줘서 작업을 확정시켜야 한다. 이것을 트랜잭션 커밋이라고 한다.

 

 JDBC 트랜잭션의 트랜잭션 경계설정.

 

모든 트랜잭션은 시작하는 지점과 끝나는 지점이 있다. 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 트랜잭션의 경계라고 부른다. JDBC 의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄진다. 자동 커밋을 꺼주면, 트랜잭션이 한 번 시작되면 commit() 혹은 rollback() 메소드가 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다. 이렇게 setAutoCommit(false) 로 트랜잭션의 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다.

 트랜잭션의 경계난 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다는 점도 기억해두자. 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬트랜잭션이라고 한다.

 

 

 JdbCTemplate은 하나의 템플릿 메소드 안에서 Datasource의 getConnection() 메소드를 호출해서 Connection 오브젝트를 가져오고, 작업을 마치면 Connection을 확실하게 닫아주고 템플릿 메소드를 빠져나온다. 결국 템플릿 메소드 호출 한 번에 한 개의 DB 커넥션이 만들어지고 닫히는 일까지 일어난다. 따라서 템플릿 메소드가 호출될 때마다 트랜잭션이 새로 만들어지고 메소드를 빠져나오기 전에 종료된다.

 이러면, 각 메소드들이 JdbcTemplate을 사용한다고 가정하면, 여러 메소드들을 하나의 트랜잭션으로 묶을 수 없다.

 

따라서 하나의 트랜잭션으로 묶기위해 직접 DB Connection을 생성하고, 트랜잭션을 시작하고, Connection 오브젝트를 각 메소드들에게 전달하는 방식을 생각할 수 있다. (마지막에는 당연히 Connection을 종료해준다.)

 하지만 이 또한 여러 문제점이 있다. 

 1. JdbcTemplate을 사용할 수 없으며 try catch finally 가 반복된다.

 2. Connection 파라미터가 계속 전달되게 생겼다. 지저분하다.

 3. Connection 파라미터가 추가되면서 더이상, 파라미터를 사용하는 객체는 데이터 액세스 기술에 "독립적일 수 없다."

 4. 테스트에도 영향을 끼친다.

...등등의 문제가 있다.

 

트랜잭션 동기화

 

그러면 Connection을 파라미터로 직접 전달하는 문제를 해결해보자. 트랜잭션 경계설정은 무슨 수를 쓰던 하긴 해야 한다. 하지만 Connection을 메소드의 파라미터로 전달하는건 피하고 싶다. 이를 위해 스프링이 제안하는 방법은 독립적인 트랜잭션 동기화 방식이다. 트랜잭션 동기화란, 트랜잭션을 시작하기 위해 만든 Connection Object를 특별한 저장장소에 보관해두고 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 것이다. 

 [트랜잭션 동기화 저장소의 활용]

 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려는 없다.

 

 스프링이 제공하는 트랜잭션 동기화 관리 클래스는 TransactionSynchronizationManager다. 이 클래스를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청한다.

 

 

트랜잭션 서비스 추상화

 

 그런데 만약 DB를 여러개 사용하고, 여러개의 DB 사이에서 이뤄지는 작업을 하나의 트랜잭션으로 묶으려면 어떻게 해야 할까? JDBC의 Connection을 이용한 트랜잭션 방식인 로컬트랜잭션으로는 불가능하다. 왜냐하면 로컬 트랜잭션은 하나의 DB Conenction 에 종속되기 때문이다. 따라서 각 DB와 독립적ㅇ로 만들어지는 Connection 을 통해서가아니라, 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 "글로벌 트랜잭션(global tranaction)" 방식을 사용해야 한다. 글로벌 트랜잭션을 적용해야 트랜잭션 매니저를 통해 여러 개의 DB가 참여하는 작업을 하나의 트랜잭션으로 만들 수 있다. 자바는 JDBC 외에 이런 글로벌 트랜잭션을 지원하는, 트랜잭션 매니저를 지원하기 위한 API인 JTA(Java Transaction API) 를 제공하고 있다.

 

 

애플리케이션은 기존의 방식대로 DB는 JDBC를, 메시징 서버라면 JMS 같은 API를 사용해서 필요한 작업을 수행한다. 단, 트랜잭션은 JDBC나 JMS API를 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리하도록 위임한다.

 

 그런데 이러면 하나의 DB만 쓰는 고객을 위해서는 JDBC를 이용한 트랜잭션 관리 코드를, 다중 DB를 위한 글로벌 트랜잭션을 필요로 하는 곳에서는 JTA를 이용한 트랜잭션 관리 코드를 적용해야 하는 상황이 된다. 또한.. 하이버네이트 같은 경우 Connection 을 직접 사용하지 않고, Session이란 것을 사용하며 독자적인 트랜잭션 관리 API를 사용한다.

 

 결국은 Connection, UserTransaction, Session/Transaction API 등에 종속되지 않게 해야한다.

 다행히도 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조다. 공통점을 뽑아서 분리시킬 수 있다.(추상화) 이렇게 하면 하위시스템이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방식으로 접근할 수 있다.

 

 트랜잭션의 경계설정을, 어떤 상황에 오던 일관적으로 하기 위해서, 트랜잭션의 경계설정을 담당하는 코드를 추상화하자. 이렇게 하면 하위 시스템이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수 있다.

 

 

스프링의 트랜잭션 서비스 추상화

 

스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다. 이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API를 이용하지 않고도, 일관된방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해진다.

 

스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager다. 

 

 JDBC 를 이용하는 경우에은 먼저 Connection 을 생성하고 나서 트랜잭션을 시작했다. 하지만PlatformTransactionmanager 에서는 트랜잭션을 가져오는 요청인 getTransaction() 메소드를 호출만 하면 된다. 필요에 따라서 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행해주기 때문이다.

 

 ** PlatformTransactionManager 로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장된다. PlatformTransactionManager를 구현한 DatasSourceTransactionManager 오브젝트는 JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리한다. 따라서 PlatformTransactionManager를 통해 시작한 트랜잭션은 JdbcTemplate안에서 사용된다.

 

 

 -> 트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책임 원칙에 위배되며, DAO가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다.

 

 

 

코드에 만족하지 말고 계속 개선하려고 노력하자.

DI는 모든 스프링 기술의 기반이 되는 핵심 엔진이자 원리이다. 

스프링을 DI 프레임워크라 부르는 이유는 외부 설정정보를 통한 런타임 오브젝트 DI라는 단순한 기능을 제공하기 때문이 아니다. 오히려 스프링이 DI에 담긴 원칙과 이를 응용하는 프로그래밍 모델을 자바 엔터프라이즈 기술의 많은 문제를 해결하는 데 적극적으로 활용하고 있기 때문이다.

 

 

 

 

블로그 이미지

맛간망고소바

,

정리하다 보니 문제가 생겼다.

중요한 문장이라고 생각되면 적으려 하다보니, 책을 옮겨적는 수준이 되가고 있다.

ㅠㅠ

 

 

  

 스프링의 핵심을 담당하는 건 바로 빈 팩토리 또는 애플리케이션 컨텍스트라고 불리는 것이다.

 

1. 애플리케이션 컨텍스트와 설정정보.

 스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(bean)이라고 부른다.

오브젝트 단위의 애플리케이션 컴포넌트를 말한다. 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다.

 

 "스프링 빈은 오브젝트 단위의 애플리케이션 컴포넌트를 말하며, 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다."

 

 스프링에서는 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리라고 부른다. 보통 빈 팩터리보다는 이를 좀 더 확장한 애플리케이션 컨텍스트(application context) 를 주로 사용한다. 애플리케이션 컨텍스트는 IoC 방식을 따라 만들어진 일종의 빈 팩토리라고 생각하면 된다.

 

 "IoC, 제어의 역전을 가능케 하는 오브젝트를 빈 팩토리라고 부른다. 대표적으로, 각 오브젝트가 사용하는 다른 오브젝트를 직접 생성하지 않는다. 스프링의 애플리케이션 컨텍스트가 직접 의존성을 설정해준다."

 

 "빈 팩터리라 말할 때는 빈을 생성하고 관계를 설정하는 IoC의 기본 기능에 초점을 맞춘 것이고, 애플리케이션 컨텍스트라고 말할 때는 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진이라는 의미가 좀 더 부각된다고 보면 된다."

 

 애플리케이션 컨텍스트는 별도의 정보를 참고해서 빈의 생성, 관계설정 등의 제어작업을 총괄한다. 애플리케이션 컨텍스트가 직접 이런 정보를 담고 있는게 아니라, 별도로 설정 정보를 담고 있는 무엇인가를 가져와 이를 활용하는 범용적인 IoC 엔진 같은 것이라고 볼 수 있다.

 

 빈 팩토리 또는 애플리케이션 컨텍스트가 사용하는 설정정보를 만드는 방법은 여러가지가 있다. 

 그 자체로는 애플리케이션 로직을 담당하지는 않지만, IoC 방식을 이용해 애플리케이션 컴포넌트를 생성하고, 사용할 관계를 맺어주는 등의 책임을 담당하는 것이 애플리케이션 컨텍스트 + 설정정보의 역할이다.

 마치 건물이 설계도면을 따라서 만들어지듯, 애플리케이션도 애플리케이션 컨텍스트와 그 설정정보를 따라서 만들어지고 구성된다고 생각할 수 있다.

 

 @Configuration 어노테이션을 해당 클래스에 붙임으로서, 스프링은 해당 클래스가 "스프링 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스" 라고 인식할 수 있다.

 

 "@Configuration: 스프링 빈 팩토리에게, 해당 클래스를 오브젝트 설정을 담당하는 클래스로 인식하게 한다."

 

그리고 오브젝트를 만들어주는 메소드에는 @Bean 이라는 어노테이션을 붙여준다.

 

 "@Bean: 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트 를 만드는 메소드라고 명시한다."

 

 

 

애플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트이다. @Configuration 이 붙은 자바 코드를 설정정보로 사용하기 위해 AnnotationConfigApplicationContext를 이용할 수 있다.

 

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

UserDao userDao = context.getBean("userDao", UserDao.class);

 

getBean() 메소드는 ApplicationContext가 관리하는 오브젝트를 요청하는 메소드이다.

 

 

2. 애플리케이션 컨텍스트의 동작방식

 

2.1 애플리케이션 컨텍스트의 기본 동작 방식

 

 스프링에서는 이 애플리케이션 컨텍스트를 IoC 컨테이너라 하기도 하고, 간단히 스프링 컨테이너라 부르기도 한다.

애플리케이션 컨텍스트는 ApplicationContext 인터페이스를 구현하는데, ApplicationContext는 빈 팩토리가 구현하는 BeanFactory 인터페이스를 상속했으므로, 애플리케이션 컨텍스트는 일종의 빈 팩토리인 셈이다.

 

 애플리케이션 컨텍스트는 애플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당한다.

 

 ApplicationContext에는 직접 오브젝트를 생성하고 관계를 맺어주는 코드가 없다. 그런 생성정보와 연관관계 정보를 별도의 설정정보를 통해 얻는다.

 @Configuration 이 붙은 클래스는 이 애플리케이션 컨텍스트가 활용하는 IoC 설정 정보이다. 클라이언트가 해당 Bean 을 원할 때, Application Context는 @Configuration 클래스에게 요청해 Bean 을 생성 및 관리하고, 이 Bean을 요청한 클라이언트에게 전달해준다.

 (일단 클라이언트는 해당 Bean 을 사용하는 객체라고 생각하자.)

 

 

 애플리케이션 컨텍스트는 자신의 빈 목록에서 클라이언트가 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메소드를 호출해서 오브젝트를 생성시킨 후 클라이언트에게 전달한다.

 

애플리케이션 컨텍스트를 사용할 때 얻는 장점.

 

애플리케이션 컨텍스트를 사용했을 때 얻는 장점은 다음과 같다.

더보기

 

 1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.

 클라이언트가 필요한 오브젝트(빈) 을 가져오기 위해 어떤 팩토리 클래스를 사용해야 하는지 알 필요가 사라진다. 실제로 해당 오브젝트를 생성하는 놈이 누군지 알필요가 없다, 스프링 관점에선  @Configuration 클래스가 뭔지 알 필요가 없다는 뜻이다.

 

 UserAddressParser 라는 오브젝트(빈) 이 있고, 이 빈을 생성하는 놈이 UserAddressParserFactory 라고 해보자.

만약 User 정보를 얻어내는 클래스가 있다고 생각해보자. UserInformationSearcher 라는 클래스가 있다.

이 UserInformationSearcher 클래느는 new UserAddressParserFactory(); 라고 하고 userAddressParserFactory.getUserAddressParser() 이런식의 흐름을 갖을 것이다.

 여기서 UserInformationSearcher 클래스가 필요한건 오직 "UserAddressParser" 인스턴스일 뿐이다. 이 걸 누가 만들었는지는 알 필요가 없다. (이런 걸 DL 이라고 부르기도 한다.)

 

2. 애플리케이션 컨텍스트는 "종합" IoC 서비스를 제공해준다.

단순히 오브젝트 생성과 다른 오브젝트와의 관계설정만 해주는 것이 아니다. 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있으며, 이에 부가적으로 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설정 방식의 다변화, 인터셉팅 등 오브젝트를 효과적으로 화룡ㅇ할 수 있는 다양한 기능을 제공한다.

 또 빈이 사용할 수 있는 기반기술 서비스나 외부 시스템과의 연동 등을 컨테이너 차원에서 제공해주기도 한다.

 

3. 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

 애플리케이션 컨텍스트의 getBean() 메소드는 빈의 이름을 이요앻 빈을 찾아준다. 타입만으로 빈을 검색하거나 특별한 애노테이션 설정이 되어 있는 빈을 찾을 수도 있다.

 

 

 

**설정정보/ 설정 메타정보 

 

 스프링 설정정보란 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. 영어로 configuration 이라고 하는데, 이는 구성정보 내지는 형상정보라는 의미이다.

 스프링의 설정정보는 IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝틀르 생성하고 구성할 때 사용된다. 

 

 

** 컨테이너

 컨테이너라는 말 자체가 IoC의 개념을 담고 있다. 따라서 이름이 긴 애플리케이션 컨텍스트 대신에 스프링 컨테이너라고 부르는걸 선호하는 사람들이 있다. (애플리케이션 컨텍스트를 그냥 스프링 컨테이너라고 부르기도 한다.)

 애플리케이션 컨텍스트는 그 자체로 ApplicationContext 인터페이스를 구현한 오브젝트를 가리키기도 한다. 애플리케이션 컨텍스트 오브젝트는 하나의 애플리케이션에서 보통 여러 개가 만들어져 사용된다. 이를 통틀어서 스프링 컨테이너라고 부를 수 있다. 때로는 이런 스프링 컨테이너를 그냥 스프링이라 부르기도 한다. 따라서 "스프링에 빈을 등록하고...라고 표현할 수도 있다."

 

오브젝트의 동일성과 동등성

 

 자바에서는 두 개의 오브젝트가 완전히 같은 동일한 오브젝트라고 말하는 것과, 동일한 정보를 담고 있는 오브젝트라고 말하는 것은 분명한 차이가 있다. 전자는 동일성 비교라고 하고 후자를 동등성 비교라고 한다.

 

더보기

 두개의 오브젝트가 동일하다면 사실은 하나의 오브젝트만 존재하는 것이고, 두 개의 오브젝트 레퍼런스 변수를 갖고 있을 뿐이다. 두개의 오브젝트가 동일하지 않고 등등하다 할 때는 두 개의 오브젝트는 완전 다른 메모리상에 존재하는 것인데, 오브젝트의 동등성 기준에 따라 두 오브젝트의 정보가 동등하다고 판단하는 것일 뿐이다.

 

 equal 메소드를 따로 구현하지 않으면 Object 의 equals() 메소드를 사용하게 되는데, Object의 equals() 메소드는 두 오브젝트의 동일성을 비교해서 그 결과를 돌려준다.

 

 

2.2 싱글톤 레지스트리로서의 애플리케이션 컨텍스트

 

 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리(singleton registry) 이기도하다.

스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다.

 

 태생적으로 스프링은 엔터프라이즈 시스템을 위해 고안된 기술이기 때문에 서버환경에서 사용될 때 그 가치가 있다. 실제로 스프링은 대부분 서버환경에서 사용된다.

 

 서버 환경에서 사용되기 때문에 매 요청마다 오브젝트를 생성하면 서버가 감당하기 힘들다. 따라서 엔터프라이즈 분야에서는 서비스 오브젝트라는 개념을 일찍부터 사용해왔다. 서블릿은 자바 엔터프라이즈 기술의 가장 기본이 되는 서비스 오브젝트라고 할 수 있다. 서블릿은 대부분 멀티스레드 환경에서 싱글톤으로 동작한다. 서블릿 클래스당 하나의 오브젝트만 만들어두고 ,사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용한다.

 

 

누군가는 싱글톤을 안티패턴이라고 부른다. 흔히들 말하는 싱글톤 패턴을 제대로 구현하면, 인스턴스는 한 애플리케이션에 하나만 존재해야 하며, 이를 위해 생성자에 private 을 붙여 두번의 생성을 막고 있으며... 등등의 제약 조건이 있기때문에,

 결과적으로 여러 문제가 생긴다.

  •  싱글톤 객체가 정말 하나만 생긴 것을 어찌 보장할 것인가?
  •  싱글톤은 전역 상태를 만들 수 있으므로 옳지 못하다.
  •  싱글톤은 상속이 안되므로, 객체지향을 파괴한다.
  •  테스트하기 힘들다..

등등의 문제로 싱글톤 패턴을 싫어하는 사람들이 많다.

 

 자바의 기본적인 싱글톤 패턴의 구현 방식은 이런 단점들이 있기 때문에ㅡ 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 그것이 바로 싱글톤 레지스트리이다.

 스프링은 스태틱 메소드와 private 생성자를 사용해야하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글토으로 활용하게 해준다.

 

 

스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다.

=>

 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다. 따라서 상태 관리에 주의를 기울여야 한다. 상태정보를 내부에 갖고 있지 않은 무상태 방식으로 만들어져야 한다.

 

 

 

2.3 의존 관계 주입(DI)

 

스프링을 단순히 IoC 컨테이너라고만 하면, 스프링이 서블릿 컨테이너처럼 서버에서 동작하는 서비스 컨테이너라는 뜻인지, 아니면 단순히 IoC개념이 적용된 템플릿 메소드 패턴을 이용해 만들어진 프레임워크인지, 아니면 또 다른 ioC 특징을 지닌 기술이라는 것인지 파악하기 힘들다. 그래서 스프링이 제공하는 IoC 방식의 핵심을 짚어주는 의존관계 주입(DI) 라는 이름을 사용하기 싲가했다.

 이제 DI 컨테이너라고 부른다.

 

 DI는 오브젝트 레퍼런스를 외부로브테 제공받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다. 

 

 

의존관계란 무엇인가? 의존한다는 것은 A -> B 일때 의존대상인 B가 변하면 그것이 A에게 영향을 미친다는 뜻이다.

 

의존관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

 

 

2.4 의존관계 검색과 주입

 

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아니다. 코드에서는 구체적인 클래스에 의존하지 않고, 런타임 시에 의존관계를 결정한다는 점에서 의존관계 주입과 비슷하지만, 의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색(Dependency Lookup) 이라고 불리는 것도 있다.

 

 물론 자신이 어떤 클래스의 오브젝트를 이용할지 정하는 것은 아니다. 의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성 작업은 외부 컨테이너에게 IoC 로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.

 

 예를들면 다음과 같이...

 

public UserDao() {

 AnnotationConfigApplicationContext context = new AnnotationConfigApplciationContext(DaoFactory.class);

 this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);

}

 

위에서 DaoFactory.class 와 ConnectionMaker.class 를 보고, 사실상 의존관계 다 드러나는 것 아닌가? 하는 생각을 할 수도 있다.

...맞다.

ConnectionMaker.class 는 그냥 인터페이스를 명시할 뿐이다, 실제로 어떤 인스턴스가 생성됐는지는 UserDao는 전혀 알지 못한다.

하지만 AnnotationContext 와 설정 정보인 DaoFactory.class 를 직접 언급하고 있다는 점이 마이너스.

 

의존관계 검색 방법은 코드 안에 오브젝트 팩토리 클래스나 스프링 API 가 나타나므로, 결국 애플리케이션 컴포넌트가 컨테이너와 같이 성격이 다른 오브젝트에 의존하게 되는 것이므로 그다지 바람직하지 않다.

 따라서 대개는 의존관계 주입 방식을 사용하는 편이 낫다.

 

의존관계 주입과 의존관계 검색에 결정적인 차이점은 하나 더 있다.

의존관계 주입과는 달리 의존관계 검색은, "Bean 을 필요로 하는 인스턴스가 Bean 일 필요가 없다."

 

 

 DI 를 원하는 오브젝트는 먼저 자신의 컨테이너가 관리하는 빈이 돼야 한다.

 

 

 

3. XML 을 이용한 설정

 XML을 이용해서도 DI 의존관계 설정정보를 만들 수 있다. XML은 단순한 텍스트 파일이기 때문에 다루기 쉬우며, 쉽게 이해할 수 있고, 컴파일과 같은 별도의 빌드작업이 필요 없다는 것 또한 장점이다. 환경이 달라져서 오브젝트의 관계가 바뀌는 경우에도 빠르게 변경사항을 반영할 수 있다.

 

 

 

4. Datasource 관련

자바에는 DB 커넥션을 가져오는 오브젝트의 기능을 추상화한 DataSource라는 인터페이스가 이미 존재한다. 이미 다양한 방법으로 DB 연결과 풀링(pooling) 기능을 갖춘 많은 Datasource 구현 클래스가 존재하고, 이를 가져다 사용하면 대부분 충분하다.

 

 

블로그 이미지

맛간망고소바

,

스프링을 다시 정리하고자 한다.

너무 스프링 부트와 관련 기술만 생각했던 것 같다.

 

정리는 이일민님의 토비의 스프링 3.1 을 이용해 정리할 것이다.

 

옛날에 한 번 읽고... 중간에 다 읽지 못한 책이다. 

그 때는 자바 언어의 기본 문법만 알고 있었다. 대학생 때였고, 그 때에는 너무 많은 것이 이해가 가진 않았다.

지금은 아주 조금이라도 더, 객체지향에 대해 잘 이해하고 있다. 새로운 마음으로 다시 정리해보겠다.

 

당연히 개인적인 정리이다보니, 스스로가 이해한 방식대로 서술할 것이다.

혹시 누군가 이 글을 읽고 의아한 부분이 있다면, 토비의 스프링을 읽어보길 바란다. (혹은 구글링)

 

1. 스프링이란 무엇인가

 

스프링은 자바 엔터프라이즈 애플리케이션 개발에 사용되는 애플리케이션 프레임워크이다. 애플리케이션 프레임워크는 애플리케이션 개발을 빠르고 효율적으로 할 수 있도록 애플리케이션의 바탕이 되는 틀공통 프로그래밍 모델, 기술 API 등을 제공해준다.

 

  •  애플리케이션의 기본 틀 -스프링 켄터이너
  •  공통 프로그래밍 모델 -IOC/DI, 서비스 추상화, AOP
  •  기술 API

 

더보기
  •  애플리케이션의 기본 틀 -스프링 컨테이너

 스프링은 스프링 컨테이너 또는 애플리케이션 컨텍스트라고 불리는 스프링 런타임 엔진을 제공한다.

스프링컨테이너는 설정정보를 참고로 해서 애플리케이션을 구성하는 오브젝트를 생성하고 관리한다.

 

  • 공통 프로그래밍 모델 -IoC/DI , 서비스 추상화, AOP

프레임워크는 애플리케이션을 구성하는 오브젝트가 생성되고 동작하는 방식에 대한 틀을 제공할 뿐만이 아니라, 애플리케이션 코드가 어떻게 작성돼야 하는지에 대한 기준도 제시해준다.

 이런 틀을 보통 "프로그래밍 모델"이라고 한다.

 

 

 1) 첫 번째는 IoC/DI 라고 불리는 오브젝트 생명주기와 의존관계에 대한 프로그래밍 모델이다.

 스프링은 유연하고 확장성이 뛰어난 코드를 만들 수 있게 도와주는 객체지향 설계 원칙과 디자인 패턴의 핵심 원리를 담고 있는 IoC/DI 를 프레임워크의 근간으로 삼고 있다. 스프링 프레임워크에서 동작하는 코드는 IoC/DI 방식을 따라서 작성돼야 스프링이 제공하는 가치를 제대로 누릴 수 있다.

 2) 두 번째는 서비스 추상화다.

 스프링을 사용하면 환경이나 서버, 특정 기술에 종속되지 않고 이식성이 뛰어나며 유연한 애플리케이션을 만들 수 있다. 이를 가능하게 해주는 것이 바로 서비스 추상화다. 구체적인 기술과 환경에 종속되지 않도록 유연한 추상 계층을 두는 방법이다.

 3) 세번째는 AOP다.

 AOP는 애플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델이다. 스프링은 AOP를 이용해서, 다양한 엔터프라이즈 서비스를 적용하고도 깔끔한 코드를 유지할 수 있게 해준다.

 

  •  기술 API

스프링은 엔터프라이즈 애플리케이션을 개발의 다양한 영역에 바로 활용할 수 있는 방대한 양의 기술 API를 제공한다.

 

 스프링을 사용한다는 것은 바로 이 세 가지 요소를 적극적으로 활용하여 애플리케이션을 개발한다는 뜻이다. 클래스는 스프링 컨테이너 위에서 오브젝트로 만들어져 동작하게 만들고, 코드는 스프링의 프로그래밍 모델을 따라서 작성하고, 엔터프라이즈 기술을 사용할 때는 스프링이 제공하는 기술 API와 서비스를 활용해주도록 하면 된다.

 

 

2. 스프링은 왜 성공하였는가? => 왜 좋은가?

 

 스프링은 갑자기 나타는게 아니다. 자바를 통해 엔터프라이즈 시스템을 개발하는 데 좀 더 나은 방법과 전략을 찾으려고 고민하던 개발자들의 수고가 집약된 결정체다. 스프링은 사실상 자바 엔터프라이즈 표준 기술이라고 여겨진다.

 견고하고 건전한 자바 엔터프라이즈 개발의 핵심 가치에 충실하다. 스프링을 사용하는 개발자들은 자연스럽게 자바와 엔터프라이즈 개발의 기본에 충실한 베스트 프랙티스를 적용할 수 있고, 이상적인 개발 첡학과 프로그래밍 모델으르 이해하게 되고, 좋은 개발 습관을 체득하게 된다.

 

 스프링을 사용하는 개발자들이 스프링을 통해 얻게되는 두 가지 중요한 가치가 있다. 바로 단순함과 유연성이다.

 

 단순함 (Simplicity)

 스프링이 지향하는 것은 목적을 이룰 수 있는 가장 단순하고 명쾌한 접근 방법이다. 자바는 이상적인 객체지향 언어라는 캐치프레이즈를 내세우며 등장했다. 자바의 기술이 복잡해져가면서 자바의 본질인 객치제향 언어라는 특징을 점점 잃어버렸다. 스프링은 이 잃어버린 객체지향 언어의 장점을 다시 개발자들이 살릴 수 있도록 도와주는 도구다. 그래서 스프링으 강력히 주장하는 것은 가장 단순한 객체지향적인 개발 모델인 POJO 프로그래밍이다.

 

 유연성 (Flexibility)

 스프링은 유연성을 중요한 가치로 내세운다. 스프링은 유연성과 확장성이 매우 뛰어나다. 스프링의 유연성으로 인해 스프링은 다른 많은 프레임워크와 편리하게 접목돼서 사용될 수 있다. 스프링만큼 많은 서드파티 프레임워크의 지원을 받는 기술도 없다. 스프링 개발 철학 중 하나는 "항상 프레임워크 기반의 접근 방법을 사용하라" 이다. 스프링 기능의 대부분은 핵심 기능을 확장해서 발전시킨 결과물이다. 스프링은 개발자들에게 스프링을 확장해서 사용하도록 권장한다. ㅅ프링을 제대로 사용하려면 스프링을 필요에 맞게 확장해서 자신만의 프레임워크를 만들어서 사용할 줄 알아야 한다.

 

 

 요약하자면, 스프링은 개발자들이 자신이 원하는 목적을 쉽게 이루게 해준다. 자신이 짜고자 하는 코드에만 집중할 수 있게 해준다. 또한 뛰어난 개발자들이 짠, 객체지향의 결정체이기 때문에 유연성과 확장성이 뛰어난 프레임 워크이다. 스프링의 뛰어난 유연성으로 인해 스프링은 많은 서드파티 프레임워크의 지원을 받는 기술이다. 이런 모든 것들이 결국 개발자들이 자신이 원하는 목적을 쉽게 우리게 해주며, 자신의 코드에 집중할 수 있게 해준다.

 

 

 

3. IOC 에 대하여

 

Inversion Of Control 의 약자이다. 제어의 역전이라는 건, 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것이라고 할 수 있다.

 일반적인 프로그램의 흐름은 main() 메소드와 같이 프로그램이 시작되는 지점에서 다음에 사용할 오브젝트를 결정하고,  결정한 오브젝트를 생성하고, 만들어진 오브젝트..... 하여간.

 프로그램 구조에서 각 오브젝트는 프로그램 흐름을 결정하거나 사용할 오브젝트를 구성하는 작업에 능동적으로 참여한다.

 

 모든 오브젝트는 능동적으로 자신이 사용할 클래스를 결정하고, 언제 어떻게 그 오브젝트를 만들지를 스스로 관장한다. 모든 종류의 작업을 "사용하는 쪽에서 제어하는 구조" 이다.

 제어의 역전이란 이런 제어 흐름의 개념을 거꾸로 뒤집는 것이다. 예를들어, 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다. 또 자신도 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어의 권한을 오브젝트 자신이 아닌 다른 대상에게 위임한 것이다.

 프로그램의 시작을 담당하는 main() 과 같은 엔트리포인트를 제외하면 모든 오브젝트는 이렇게 위임받은 제어 권한을 갖는 특별한 오브젝트에 의해 결정되고 만들어진다.

 

-예시-

 

더보기

 1. main() 메소드를 기반으로 제어되는게 아닌 코드

 원래는 main() 메소드에서 시작해서 개발자가 미리 정한 순서를 따라 오브젝트가 생성되고 실행되는 것이 일반적인 자바 프로그램의 흐름이다. 서블릿을 생각해보자. 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고 그 안의 메소드를 호출한다. 이렇게 서블릿이나 JSP, EJB처럼 컨테이너 안에서 동작하는 구조도 간단한 방식이지만 제어의 역전 개념이 들어가 있는 것이다.

 -> 일반적으로는 main() 에서 시작돼야 하는데, 서블릿을 실행 시키는 것은 main 메소드로부터가 아니라, 다른 컨테이너에 의해 실행됨. 제어의 권한을 컨테이너에게 넘김. 이 또한 제어의 역전이다. (물론 이 컨테이너 자체는 main 메소드로 인해 실행 됐겠지만)

 

 2. 디자인 패턴에서 나타나는 IoC

 템플릿 메소드 패턴과 같이, 추상 클래스를 상속한 하위 클래스의 메소드가 있다고 생각 해보자.  이 메소드는 애초에 자신이 어떻게 사용될지 모른다. 슈퍼 클래스의 템플릿 메소드에서 필요할 때 호출해서 사용한다. 즉 제어권을 상위의 템플릿 메소드에게 넘기는 것이다.

 -> 일반적으로 객체는 자신의 메소드를 자신이 호출한다. 헷갈릴 수 있으니 조금 더 정리해보자.

 A -> B 의 의존관계가 있고, A 가 B 의 메소드를 호출할 수 있다. 이 때 B의 메소드를 호출하는 것은 누구인가? A이긴 하지만 이 메소드 자체는 분명 B 오브젝트가 실행한다. 이 경우의 이 메소드의 제어건은 B에 있다. 애초에 객체지향의 객체는 자신의 메소드가 어떻게 실행되는지는 자신이 결정하는 것이 정상적이다.

 하지만 템플렛 메소드 패턴에서는 A 가 B의 메소드를 호출할 때, 사실은 B의 슈퍼클래스의 메소드를 호출한 것이며, 이 때 B에서 override해서 구현한 훅 메소드(혹은 추상 메소드) 는 B의 슈퍼클래스의 메소드에 의해 사용된다. 이 순간 B는 자신의 메소드가 어떻게 실행될 것인지를, 자신이 컨트롤 하지 않는다. 따라서 제어의 역전이라 할 수 있다.

 

 3. 프레임워크

 프레임워크도 제어의 역전 개념이 적용된 대표적인 기술이다. 프레임워크는 라이브러리의 다른 이름이 아니다. 라이브러리를 사용하는 애플리케이션의 코드는 애플리케이션의 흐름을 직접 제어한다.  하지만 프레임워큰느 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 프레임 워크 위에 갭라한 클래스를 등록해두고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만드는 방식이다. 개나소나 다 프레임워크로 불려도 되는 것이 아니다. 프레임워크에는 분명한 제어의 역전 개념이 적용되어 있어야 한다.

 

이 뒤는 스프링의 IoC 로부터 다시 정리할 생각이다. 내용이 무지 방대 하므로 다음 편으로 넘기도록 하겠다.

 

 

 

 

 

 

 

블로그 이미지

맛간망고소바

,

객체지향의 사실과 오해

The Essence of Object-Orientation 저자 -조영호-

 

좋은 책이었다. 재미있었다.

최근 열심히 공부해야겠다는 생각을 하고 있었는데, 선임분이 추천해줘서 사서 읽게 됐다.

책을 워낙 더럽게 읽어서 (밑줄을 친다던지, 필기를 한다던지), 구매해서 읽었다.

 

언제 한번 SQL 관련된 책도 한번 봐야 하는데, 사 놓기만 하고 읽지는 않고 있다.

반성하자.

 

목차만 봐도 내용이 얼핏 기억이 날 것 같다. 따라서 목차만 대충 적어놓자.

 

1. 협력하는 객체들의 공동체

 -역할, 책임, 협력.

 -협력 속에 사는 객체

 -자율성

객체지향이란 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고, 객체를 이용해 시스템을 분할하는 방법이다.

자율적인 객체란 상태와 행위를 함께 지니며 스스로 자기 자신을 책임지는 객체를 의미한다.

객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력한다. 각 객체는 협력 내에서 정해진 역할을 수행하며 역할은 관련된 책임의 집합이다.

...

객체 지향의 본질은 객체다. 클래스가 아니다.

핵심은 적절한 책임을 수행하는 역할 간의 유연하고 견고한 협력 관계를 구축하는 것이다.

클래스는 협력에 참여하는 객체를 만드는 데 필요한 구현 메커니즘일 뿐이다.

 

2. 이상한 나라의 객체

값객체와 참조객체는 다르다. 참조객체는 엔티티, 즉 식별자를 지닌 전통적인 의미의 객체를 가리키는 용어이다.

 -객체는 상태를 가지며 상태는 변경 가능하다.

 -객체의 상태를 변경시키는 것은 객체의 행동이다.

 -객체는 어떤 상태에 있더라도 유일하게 식별 가능하다.

(위 객체는 모두 엔티티)

버튼을 누른건 사용자지만... 버튼에 따라 어떻게 작동할지는 기계 스스로 결정한다...

객체에 접근할 수 있는 유일한 방법은 객체가 제공하는 행동뿐

 

행동이 상태를 결정한다. 상태를 먼저 정의하고 행동을 정의하는 것이 아니라, 행동을 먼저 정의해야 한다.

협력에 참여하는 훌륭한 객체 시민을 양성하기 위한 가장 중요한 덕목은 상태가 아니라 행동에 초점을 맞추는 것이다.

 

객체지향의 설계는 애플리케이션에 필요한 협력을 생각하고 협력에 참여하는 데 필요한 행동을 생각한 후 행동을 수행할 객체를 선택하는 방식으로 수행한다.

 

객체의 행동은 결국 객체가 협력에 참여하면서 완수해야할 책임을 의미하며. 어떤 책임이 필요한가를 결정하는 과정이 전체 설계를 주도해야 한다. 

 

객체지향은 현실세계의 모방이긴 한데, 추상화가 아니라 은유라고 표현하는 것이 더 어울린다.

객체지향은 현실세계를 은유한다. (의인화)

 

3. 타입과 추상화

현실 세계는 복잡하다. 이 복잡성을 낮출 필요가 있다. 

추상화는 현실에서 출발하되 불필요한 부분을 도려내가면서 사물의 놀라운 본질을 드러나게 하는 과정이라고 할 수 있다.

추상화는 공통점을 취하고 차이점을 버리는 일반화, 세부사항을 버리는 단순화 두가지 측면을 갖는다.

 

개념: 공통점을 기반으로 객체들을 묶기 위한 그릇. 

 

개념을 이용해서 객체를 여러 그룹으로 "분류" 할 수 있다.

개념에는 세 가지 관점이 있다. 심볼(이름), 내연(의미), 외연(각 포함되는 놈들)

 

분류란 객체에 특정한 개념을 적용하는 과정이다.

 

개념은 추상화의 첫 번째 차원인 "일반화"를 적용한 결과다.

 

타입은 결국 개념이다.

타입은 공통점을 기반으로 객체를 묶기 위한 틀이다.

정통적인 데이터 타입은 객체의 타입과 많이 유사하다.

 

1.어떤 객체가 어떤 타입에 속할지 정하는 것은, 객체가 수행하는 행동이다.

어떤 객체들이 동일한 행동을 할 수 있다면 둘은 같은 타입이다.

2.객체의 내부적인 표현은 외부로부터 철저히 감춰진다. 객체의 행동을 가장 효과적으로 수행할 수만 있다면 객체 내부의 상태를 어떤 식으로 표현해도 무방하다.

 

결국 행동이 중요하다.

객체의 행동은 결국 책임을 의미한다고 했다. 결과적으로 동일한 책임을 수행하는 일련의 객체는 동일한 타입으로 볼 수 있다.

객체가 어떤 데이터를 갖는지는 우리의 관심사가 아니다. 동일한 메세지를 수신할 수 있는 놈이기만 하면 된다.

 

훌륭한 객체지향 설계는 외부에 행동만을 제공하고 데이터는 행동 뒤로 감춰야 한다. 흔히 캡슐화라고 한다.

객체를 결정하는 것은 행동이다. 데이터는 단지 행동을 따를 뿐이다.

 

결국 타입은 추상화다. 타입을 이용하면 객체의 동적인 특성을 추상화할 수 있다. 결국 타입은 시간에 따른 객체의 상태 변경이라는 복잡성을 단순화할 수 있는 효과적인 방법이다.

 

클래스와 타입을 혼동하지 말자. 클래스는 타입의 구현 외에도 코드를 재사용하는 용도로도 사용되기 때문에, 클래스와 타입을 동일시하는 것은 수많은 오해와 혼란을 일으킨다.

 

타입을 나누는 것은 객체의 행동이다. 객체를 분류하기 위해 타입을 결정한 후, 프로그래밍 언어를 이용해 타입을 구현할 수 있는 한 가지 방법이 클래스일 뿐이다.

 

4. 역할, 책임, 협력

객체의 책임은 객체가 무엇을 알고 있는가, 무엇을 할 수 있는가로 구성된다.

객체지향에서 책임은 무진장 중요하다.

 

객체가 수행하는 책임의 집합은 객체가 협력 안에서 수행하는 역할을 암시한다.

역할을 대체할 수 있는 객체는 동일한 메시지를 이해할 수 있는 객체로 한정된다.

역할의 개념을 사용해 유사한 협력을 추상화해서 인지 과부하를 줄일 수 있다.

본질적으로 역할은 다른 객체에 의해 대체 가능함을 의미한다.

객체는 역할이 암시하는 책임보다 더많은 책임을 가질 수 있다.

역할이 협력을 추상적으로 만들 수 있는 이유는 역할 자체가 객체의 추상화이기 때문이다.

 

올바른 객체를 설계하기 위해서는 먼저 견고하고 깔끔한 협력을 설계해야 한다.

 

요청과 응답의 흐름 -> 책임 할당 -> 각 객체가 외부에 제공해야할 행동 -> 데이터 고민 -> 클래스 구현 방법 결정.

협력이라는 문맥을 절대 잊지 말자.

 

5. 책임과 메세지.

너무 구체적 메세지와 너무 추상적인 메세지는 둘 다 안좋다.

너무 구체적인 책임을 요구하면, 객체의 자율성이 떨어진다.

 

추상적이고 포과렂ㄱ인 책임은 협력을 좀 더 다양한 환경에서 재사용할 수 있도록 유연성이라는 축복을 내려준다. 그러나 책임은 협력에 참여하는 의도를 명확하게 설명할 수 있는 수준 안에서 추상적이어야 한다.

 

어떻게가 아니라 무엇을 할 지를 명령하라.

 

한 객체의 책임은, 객체가 수행하는 행동이며, 이 행동이 실행되는 때는 다른 객체의 "메세지" 가 전달돼야 한다. 

 

어떤 객체에게 메시지를 전송하면 결과적으로 메시지에 대응하는 특정 메서드가 실행된다.

메시지는 어떻게 수행될 것인지는 명시하지 않는다. 어떤 메서드를 선택할 것인지는 전적으로 수신자의 결정에 좌우된다.

 

메시지를 수신한 객체가 실행 시간에 메서드를 선택할 수 있다는 사실은 다른 프로그래밍 언어와 객체지향 프로그래밍 언어를 구분 짓는 핵심적인 특성 중 하나다.

 

다형성이란 서로 다른 유형의 객체가 동일한 메시지에 대해 서로 다르게 반응하는 것을 의미한다.

기본적으로 다형성은 동일한 역할을 수행할 수 있는 객체들 사이의 대체 가능성을 의미한다.

 

책임-주도 설계 방식에서는 협력에 참여할 객체를 결정하기 전에, 협력에 필요한 메세지를 먼저 결정한다.

(어떤 메세지가 필요할까? -> 이제 이건 누가 수신하고 처리하지?)

 

객체가 다른 객체의 상태를 묻는다는 것은 메시지를 전송하기 이전에 객체가 가져야 하는 상태에 관해 너무 많이 고민하고 있다는 증거다.

결과적으로 묻지 말고 시켜라. 

 

인터페이스와 구현의 분리

 

소프트웨어는 항상 변경되기 때문.

 

캡슐화 (정보은닉)

 

상태와 행위의 캡슐화

 객체는 상태와 행동을 하나의 단위로 묶는 자율적인 실체. 객체는 상태와 행위를 한 데 묶고, 외부에서 반드시 접근해야만 하는 행위만 골라 공용 인터페이스를 통해 노출한다. 따라서 데이터 캡슐화는 인터페이스와 구현을 분리하기 위한 전제조건이다.

 객체지향에서는 데이터와 프로세스를 객체라는 하나의 틀 안으로 함께 묶어 놓음으로써 객체의 자율성을 보장한다.

사적인 비밀의 캡슐화

 공용 인터페이스만 노출.

 

객체의 외부와 내부를 명확하게 구분하라. 그러면 설계가 단순하고 유연하고 변경하기 쉬워질 것이다.

 

6. 객체 지도

안정적인 도메인 모델을 기반으로, 기능을 구현하라.

기능을 정리하는데 유스케이스 모델을 사용할 수 있다.

 

도메인 모델 -> 안정적인 구조 제공.

안정적인 구조를 기반으로 기능 개발 필요.

 

책임 할당의 기본 원칙은 책임을 수행하는 데 필요한 정보를 가진 객체에게 그 책임을 할당하는 것이다.

 

7. 함께 모으기

 

서브클래스와 슈퍼 클래스.

 

구조적 순응: 서브클래스가 슈퍼클래스와 같은 속성 갖고 있음 

행위적인 순응을 흔히 리스코프 치환원칙이라고 한다.

(Person 의 getAge() , Employee 의 getAge())

서브타이핑과 서브클래싱은 다르다.

서브클래스가 슈퍼클래스를 대체할 수 있는 경우를 서브타이핑이라고 하고, (설계의 유연성을 위함)

대체 못하는 경우를 서브 클래싱이라고 한다. (코드 중복 제거와 재사용 목적)

 

 

 

중요해보이는 것만 한번 적어보려다가 난리가 났다.

위의 내용은 책과 완전히 일치하진 않는다. 내가 멋대로 해석해서 적은 내용도 있다.

 

그리고 이해가 안가는 부분이 많을 수도 있다.

내가 다시 봤을 때, 연상 될 수 있는 방식으로 글을 적었기 때문이다.

 

하여간 이렇게 책 한권을 읽었다.

좋은 책이었다. 감사합니다 영호님.

블로그 이미지

맛간망고소바

,