728x90
반응형

이전에 웹 브라우저의 세션을 이용해서 로그인과 회원가입을 구현하고 쿠키에 저장하는 방법을 통하여 로그인 회원가입을 처리해본 경험이 있다.
하지만 세션과 쿠키를 사용하는 것에는 단점들이 있다. 먼저 jwt는 STATELESS하므로 상태를 저장하지 않는 RESTful API에 적합하였고, 모바일 환경에서도 쉽게 사용할 수 있다는 장점이 있었다.

해당 내용을 바탕으로 JWT와 스프링 시큐리티를 사용하여 로그인, 회원가입, JWT 토큰을 이용한 나의 정보 확인 이렇게 3가지 기능을 동작하도록 만들었다.

먼저 3가지 기능에 해당하는 내가 만든 API endpoint는 다음과 같다.

  1. /members/login
  2. /members/signup
  3. /members/check

이제 스프링 시큐리티와 JWT를 알아보면서 해당 기능들을 구현해보겠다.

스프링 시큐리티, 사용이유

원리 : 서블릿 필터를 이용한다.

Spring의 내부 원리는 사용자(client)가 api를 호출하면 servlet container(tomcat)가 받고, request 받은 것을 spring의 dispatcherservlet에게 요청하게 된다. 요청에 따라서 controller에게 전달하고 서블릿 컨테이너가 응답을 받아서 사용자에게 전달한다.

servlet container에서 spring dispatcher servlet까지 전달될때까지 개입을 할 수 있는데, 개입을 필터를 적용해서 할 수 있다. 즉, servlet 객체들이 dispatcher servlet에 도착하기 전에 필터체인을 통과한 뒤에 dispatcherservlet에 도착하게 되는 것이다.

따라서 필터에서는 DispatcherServlet의 빈에 직접적으로 접근할 수 없다. 그러나 Spring Security의 SecurityFilterChain을 사용하는 경우에는 DispatcherServlet의 빈에 접근할 수 있다.

Spring Security는 내부적으로 DispatcherServlet과 연동하여 보안 관련 작업을 처리하기 때문에, 필터 체인을 통과한 후에 DispatcherServlet의 빈에 접근할 수 있다. 스프링 시큐리티와 DispatcherServlet 간에는 위임이 필요하며, SecurityFilterChain을 통해 이러한 위임이 이루어진다.

(chain이라는 말은 여러 개의 연결고리라는 뜻인데, 필터를 여러 가지 등록하는 것을 chain을 따라 인증을 진행한다고 생각하면 된다.)

개발자가 spring security를 해서 구현하는 방법이 여러 가지이다. filter 자체를 이해해서 filter를 구현하는 방법이 있고, 필터에서 돌아가는 여러 가지 함수 메소드들을 오버라이딩해서 일부를 커스터마이징할 수도 있다. (나는 일부 흐름만 커스터마이징하였다.)

JWT

JWT는 JSON Web Token의 약자이다. 정보를 JSON 형태로 전송하는 토큰이라는 의미이다. JWT는 URL로 이용할 수 있는 문자열로만 구성되어 있고 서버와의 통신에서 사용된다.

JWT의 구조

  • 헤더(Header) : 검증과 관련된 내용, 해싱 알고리즘과 토큰 타입 지정
  • 내용(Payload) : 토큰에 담는 정보. 여기에 포함된 속성들은 클레임이라고 한다.
  • 서명(Signature) : 인코딩된 헤더, 인코딩된 내용, 비밀키, 헤더의 알고리즘으로 서명이 생성됨.

https://jwt.io/#debugger-io
여기에 들어가면 JWT를 생성할 수 있고 디코딩된 정보도 볼 수 있다.

스프링 시큐리티와 JWT 적용하기

UsernamePasswordAuthenticationFilter를 통한 인증 과정

  1. 클라이언트가 사용자의 아이디와 비밀번호를 포함한 인증 요청을 서버로 전송한다.
  2. 요청을 받게 되면 서블릿 필터에서 SecurityFilterChain으로 작업이 위임된다. (DispatcherServlet의 bean에 접근하기 위해)
  3. SecurityFilterChain 중에서 AuthenticationFilter에서 인증을 처리한다. 아무 설정하지 않았으면 AuthenticationFilter 중에서 UsernamePasswordAuthenticationFilter가 인증을 처리한다.
  4. AuthenticationFilter는 요청받은 username이랑 password를 가지고 UsernamePasswordAuthenticationToken에서 토큰을 생성한다.
  5. 그러면 만든 토큰을 AuthenticationManager에게 전달한다. AuthenticationManager는 인터페이스이고 이를 구현한 것은 ProvideManager이다.
  6. ProvideManager는 인증을 해주어야 하기 때문에 AuthenticationProvider로 토큰을 전달한다. 해당 Provider에서 인증을 수행하고 성공하게 되면 ProvideManager로 권한을 담은 토큰을 전달한다.
  7. AuthenticationProvider는 UserDetailsService에 토큰의 정보를 전달한다.
  8. UserDetailsService는 전달받은 정보를 통해 데이터베이스에서 일치하는 사용자를 찾아서 UserDetails 객체를 생성한다
  9. 생성된 UserDetails객체는 다시 AuthenticationProvider로 전달되고, Provider에서 인증이 성공하면 ProviderManager로 권한을 담은 토큰을 전달하게 된다.
  10. 그럼 ProviderManager는 다시 검증된 토큰을 AuthenticationFilter로 전달한다.
  11. AuthenticationFilter는 토큰을 SecurityContextHolder에 있는 SecurityContext에 저장한다.

UsernamePasswordAuthenticationFilter는 접근 권한을 확인하고 인증이 실패할 경우 화면을 보내는 역할을 한다.

이전에 SpringSecurity와의 변경점(feat. WebSecurityConfigureAdapter 상속받는 방법 없어짐)

스프링 공식문서에 나와있는 JWT 적용방식을 응용하여서 구현해보겠다. JWT 샘플코드는 아래의 것을 사용하였다.
https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/jwt/login

SecurityConfiguration 구현

일단 책이랑 정보를 찾아보았을 때 WebSecurityConfigureAdapter를 상속받아서 스프링 시큐리티를 설정한다고 하였으나 실제로 IntelliJ에서 해당 클래스가 deprecated되어서 현재는 위의 document를 응용해서 구현하였다. (Security 5.7부터 바뀌었다고 한다)
Spring Blog에도 해당 내용이 나와있다.
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
(기존에는 WebSecurityConfigurerAdapter를 상속받아 설정을 오버라이딩 하는 방식이었는데 바뀐 방식에서는 상속받아 오버라이딩하지 않고 모두 Bean으로 등록을 한다.)

document에서는 authorize.anyRequest().authentication()으로 되어있다. 그렇게 되면 로그인 하지 않으면, 권한이 없으므로 login 페이지로 계속 이동하기 때문에 일단 permitAll()을 해놓고 login 페이지가 잘 보이는 것을 확인하고 구현을 시작하였다. 공식문서의 SecurityConfig 페이지는 아래와 같고, 여기서 rule을 추가시키겠다.

@Configuration  
public class SecurityConfig {  
    @Bean  
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
    http  
        .authorizeHttpRequests((authorize) -> authorize  
        .anyRequest().permitAll()  
    )  
        .httpBasic(withDefaults())  
        .formLogin(withDefaults());  
    return http.build();  
    }  
}

Rule 추가하기

위의 코드에서는 anyRequest()가 되어 있어서 어떤 url이든 인증이 필요하다. anyRequest()를 mvcMatchers()(아니면 antMatchers사용)를 사용하고 안에 parameter로 url을 넣을수도 있고, 아니면 HTTPMethod중 GET이나 POST 같은 것들로 지정해줄 수도 있다. 로그인과 회원가입, swagger 문서들은 jwt 토큰이 있을 수가 없다. 그래서 해당 url은 인증의 범위에서 제외시켜주었다.

// SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/members/login").permitAll()
                .antMatchers("/members/signup").permitAll()
                .antMatchers("/members/check").permitAll()
                .antMatchers("/swagger-ui/index.html").permitAll()
                .anyRequest().authenticated()
                .and()
                    .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}
  • httpBasic().disable() : Spring Securtiy를 처음 의존성을 추가하면 BASE64를 이용하여 비밀번호가 나온다(https://jobdong7757.tistory.com/205 참고) 그걸 disable하는 내용이다. (사용안하고 jwt 사용할 거니까)
  • csrf().disalbe() : 쿠키 기반이 아니라 jwt 기반이므로 사용하지 않는다
  • `sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) : Spring Security 세션 정책 : 세션을 생성 및 사용하지 않는다.
  • antMatchers({허용할 endpoint}) : 여기에는 인증이 필요없는 endpoint를 작성해서 permitAll() 해주면 된다.
  • addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) : JWT 인증 필터UsernamePasswordAuthenticationFilter.class 앞에 적용하겠다는 의미로 jwtTokenProvider를 이후에 작성할 예정이다.

PasswordEncoder

PasswordEncoder는 스프링 시큐리티에서 비밀번호를 안전하게 저장하고 검증하기 위해 사용되는 인터페이스이다. PasswordEncoder를 구현한 구체적인 클래스는 비밀번호를 해싱하고, 솔트(salt)를 적용한다.

PasswordEncoderFactories.createDelegatingPasswordEncoder()는 스프링 시큐리티에서 제공하는 PasswordEncoder의 구현체를 생성하는 메서드이다. 이 메서드는 현재 스프링 시큐리티의 기본 비밀번호 인코더를 생성하고 반환한다.

주로 사용자의 비밀번호를 저장하기 전에 해당 비밀번호를 해싱하여 안전하게 저장하고, 로그인 시 비밀번호를 검증하는 등의 작업에 PasswordEncoder를 사용할 수 있다.

jwt 토큰과 jwt 인증 필터를 어떻게 사용하면 될까

jwt 토큰을 어떻게 사용할지 정리해보겠다. (이메일을 아이디처럼 사용할 것이다. 회원의 데이터베이스에 id로 저장되는 것과 다른 것이다.)
먼저, 회원가입을 할 때는 이메일과 비밀번호만 받으면 된다. 해당 정보를 입력하였을 때, 나는 회원가입이 성공하면 id, 이메일, 생성시간 3가지를 전달받는다.

그리고 로그인을 할 때는 이메일, 비밀번호를 입력하면 된다. 그럼 나는 반환 값으로 이메일, grantType(여기서는 Bearer), accessToken, refreshToken같은 인증 토큰을 전달받으면 된다.

그럼 이후부터 api 요청을 할 때, accessToken을 header에 넣어서 보내야 api가 요청이 되게 한다. 그 의미는 인증된 사용자라는 뜻이기 때문이다.

accessToken과 refreshToken을 따로 두는 것은, accessToken이 탈취될 위험이 있기 때문에, accessToken의 기한을 짧게 두고 해당 기한이 만료되면 새로 발급받기 위해 기한이 긴 refresToken을 이용해서 새로 발급받으면 되기 때문이다.

그럼 하나하나 구현을 해보겠다.

Member Controller 구성

먼저 로그인이랑 회원가입을 할 수 있도록 controller를 구성한 다음, service를 구현할 생각을 하였다. (맨 처음에 적용할 endpoint 적용)

@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
    private final MemberService memberService;

    @PostMapping("/login")
    @ResponseStatus(HttpStatus.ACCEPTED)
    public MemberLoginResponseDto login(@RequestBody MemberLoginRequestDto memberLoginRequestDto) {
        String email = memberLoginRequestDto.getEmail();
        String password = memberLoginRequestDto.getPassword();
        return memberService.login(email, password);
    }

    @PostMapping("/signup")
    @ResponseStatus(HttpStatus.CREATED)
    public MemberSignUpResponseDto signup(@Valid @RequestBody MemberSignUpRequestDto memberSignUpRequestDto) {
        String email = memberSignUpRequestDto.getEmail();
        String password = memberSignUpRequestDto.getPassword();
        return memberService.signup(email, password);
    }

    @GetMapping("/check")
    @ResponseStatus(HttpStatus.OK)
    public MemberSignUpResponseDto checkMember(HttpServletRequest request) {
        return memberService.checkMember(request);
    }

}

회원가입

회원가입을 먼저 구현해 보겠다. 위의 controller에서 memberService의 signup함수를 구현하면 된다.
일단 중복 이메일을 체크해야 하므로, 중복된 이메일을 처리해주었다. 그리고 아직은 role에 대한 것을 해야할 요구조건이 없으므로 Role은 USER로 저장해주었다. (그럼 모든 권한이 가능하게 아래에서 처리하게 하였다.)

// MemberService의 signup 함수
    @Transactional
    public MemberSignUpResponseDto signup(String email, String password) {
        // 중복된 이메일 체크
        if (memberRepository.findByEmail(email).isPresent()) {
            throw new RuntimeException("중복된 이메일이 존재합니다");
        }
        Member member = Member.builder()
                .email(email)
                .password(password)
                .createdAt(LocalDateTime.now())
                .updatedAt(LocalDateTime.now())
                .build();
        Member savedMember = memberRepository.save(member);
        MemberSignUpResponseDto responseDto = new MemberSignUpResponseDto(savedMember.getId(), savedMember.getEmail(), savedMember.getCreatedAt());
        return responseDto;
    }

먼저 중복된 이메일을 체크하고, Member 도메인의 builder 패턴으로 입력받은 이메일과 비밀번호로 만들어준다.

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Member extends BaseEntity {
    @Id @Column(updatable = false, unique = true, nullable = false, length = 250)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String email;

    @Column(nullable = false, length = 250)
    private String password;

    @CreatedDate
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    LocalDateTime createdAt;

    @LastModifiedDate
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    LocalDateTime updatedAt;

    @Builder
    public Member(String email, String password, LocalDateTime createdAt, LocalDateTime updatedAt) {
        this.email = email;
        this.password = password;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }
}

UserDetails
UserDetails 인터페이스는 스프링 시큐리티에서 사용자의 정보와 권한을 제공하기 위한 인터페이스이다. 사용자의 인증과 권한 부여를 처리하는 과정에서 스프링 시큐리티가 UserDetails 객체를 통해 사용자 정보를 관리하고 제공한다. (하지만, 현재로서는 권한을 부여할 필요는 없어서 해당 내용은 제외하였다.)

UserDetails 인터페이스는 다음과 같은 주요 메서드를 정의하고 있다.

  1. getUsername(): 사용자의 고유한 식별자인 사용자 이름을 반환
  2. getPassword(): 사용자의 비밀번호를 반환
  3. getAuthorities(): 사용자의 권한 목록을 반환. 각 권한은 GrantedAuthority 인터페이스를 구현한 객체로 표현된다.
  4. isAccountNonExpired(): 사용자 계정의 유효 기간이 만료되었는지 여부를 반환
  5. isAccountNonLocked(): 사용자 계정이 잠겨있는지 여부를 반환
  6. isCredentialsNonExpired(): 사용자의 인증 정보(비밀번호)의 유효 기간이 만료되었는지 여부를 반환
  7. isEnabled(): 사용자 계정이 활성화되었는지 여부를 반환

UserDetails 인터페이스를 통해 애플리케이션에서 보안 관련 작업을 보다 쉽게 구현할 수 있다.

로그인 구현하기

로그인은 이메일과 비밀번호를 받으면 인증을 처리하고 jwt token을 만들어주어야 하기 때문에 처음 배우는 부분이 많았다. 하나하나 차례대로 보겠다.

MemberService에 있는 login함수

@Transactional  
public MemberLoginResponseDto login(String email, String password) {  
    // 1. Login ID/PW 를 기반으로 Authentication 객체 생성  
    // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);  

    // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분  
    // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행  
    Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);  

    // 3. 인증 정보를 기반으로 JWT 토큰 생성  
    TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);  

    return new MemberLoginResponseDto(email, tokenInfo.getGrantType(), tokenInfo.getAccessToken(), tokenInfo.getRefreshToken());  
}
  • UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password) : UsernamePasswordAuthenticationToken 객체는 사용자의 인증 정보를 담고 있으며, 인증 매니저에게 전달하여 실제로 사용자를 인증하는 과정에서 사용된다.
  • Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); :
    AuthenticationFilter는 생성한 authenticationToken AuthenticationManager에게 전달한다. AuthenticationManager은 실제로 인증을 처리할 여러개의 AuthenticationProvider를 가지고 있다. 그럼 UserDetailService에서 해당 내용을 기반으로 조회를 한다.

UsernamePasswordAuthenticationToken 클래스
UsernamePasswordAuthenticationToken은 스프링 시큐리티에서 사용자의 인증을 처리할 때 사용되는 객체이다. 주로 사용자가 제출한 아이디(여기서는 email)와 비밀번호(password) 정보를 포함하여 인증 작업에 필요한 정보를 전달하는 데 사용된다.

UsernamePasswordAuthenticationToken은 인증 매커니즘에서 인증을 수행하는 과정에서 생성되고 사용된다. 일반적으로 사용자가 로그인 페이지에서 사용자 이름과 비밀번호를 입력하면, 스프링 시큐리티는 UsernamePasswordAuthenticationToken 객체를 생성하여 사용자가 제출한 인증 정보를 담아 인증 처리를 시작한다.

UsernamePasswordAuthenticationTokenAuthentication 인터페이스를 구현한 클래스로, 사용자의 인증 정보를 담고 있다. Authentication은 스프링 시큐리티에서 인증에 필요한 정보를 캡슐화하는 인터페이스이다.

UsernamePasswordAuthenticationToken은 주로 AuthenticationManager에 의해 처리되며, 인증 과정에서 제공된 사용자 이름과 비밀번호를 사용하여 사용자를 인증한다. 인증이 성공하면 Authentication 객체가 인증된 상태로 반환되고, 인증 실패시에는 예외가 발생할 수 있다.

CustomUserDetailsService

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String email) throws RuntimeException {
        return memberRepository.findByEmail(email)
                .map(this::createUserDetails)
                .orElseThrow(() -> new RuntimeException("해당하는 유저를 찾을 수 없습니다."));
    }

    // 해당하는 User 의 데이터가 존재한다면 UserDetails 객체로 만들어서 리턴
    private UserDetails createUserDetails(Member member) {
        return User.builder()
                .username(member.getEmail())
                .password(passwordEncoder.encode(member.getPassword()))
                .build();
    }
}

CustomUserDetailsService 클래스가 Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); 에서 동작되는 원리

  1. AuthenticationManager는 인증 요청에 필요한 사용자 정보를 DB에서 가져와야 한다. 이때 loadUserByUsername 메서드가 호출된다.
  2. loadUserByUsername 메서드는 UserDetailsService 인터페이스를 구현한 CustomUserDetailsService에서 구현된다. 해당 메서드는 주어진 사용자 이름(username)을 기반으로 사용자 정보를 조회하고, UserDetails 객체를 반환한다.
  3. AuthenticationManagerloadUserByUsername 메서드를 통해 반환된 UserDetails 객체를 사용하여 인증 과정을 진행한다. 인증 과정에서는 사용자가 입력한 인증 정보와 UserDetails 객체에 저장된 정보를 비교하여 인증을 수행한다.
  4. AuthenticationManager는 인증 결과인 Authentication 객체를 생성하여 반환한다. 이때 Authentication 객체는 인증된 사용자 정보와 권한 정보를 포함한다.

이제 DB에 사용자가 있다는 사실을 인증받은 것이므로 해당 내용을 바탕으로 jwtToken을 생성하는 jwtTokenprovider를 만들면 된다.

토큰 정보 클래스 만들기

클라이언트가 요청하면 서버는 jwt token을 클라이언트에게 response로 주어야 한다.
해당 정보를 클래스로 만들어보자.

@Builder  
@Data  
@AllArgsConstructor  
public class TokenInfo {  
    private String grantType;  // Bearer
    private String accessToken;  
    private String refreshToken;  
}

grantType
grant typedms 클라이언트가 어떤 유형의 인증 흐름을 사용하여 액세스 토큰을 요청하는지 나타낸다.
그 중에서 Bearer를 사용할 건데, Bearer 토큰은 서버에 상태를 저장하지 않고, 클라이언트가 요청을 보낼 때마다 토큰의 유효성을 검증한다. 그래서 이후에 HTTP 요청을 보낼때 헤더에 붙여서 보내면 인증된 사용자인지 검증할 수 있는 것이다.

JwtTokenProvider 클래스

JwtTokenProvider는 사용자 인증과 인가를 처리하는 데 사용되는 클래스이다. JWT (JSON Web Token)을 생성, 유효성 검사 및 해석하는 데 사용되고, 주로 Spring Security와 함께 사용되며, 사용자의 인증 정보를 JWT 토큰으로 인코딩하여 전달하고, 토큰의 유효성을 검사하여 사용자를 인증하고 권한을 부여한다. secret key를 사용해서 jwt token을 만들 것이다.

yml 파일에 secret key 추가

토큰을 암호화하고 복호화할 때 HS256 알고리즘을 사용하려면 secret key를 설정해주어야 한다. 해당 코드를 yml 파일에 적어주자. (아무거나 해당 길이만 맞으면 된다!)

jwt:  
    secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa

JwtTokenProvider 클래스

// 토큰 생성  
@Slf4j
@Component
public class JwtTokenProvider {

    private final Key key;

    @Value("${jwt.secret}")
    String salt;

    private final long at_exp = 1000L * 60 * 30;
    private final long rt_exp = 1000L & 60 * 60;
    // secret key를 가지고 key값 저장
    public JwtTokenProvider(@Value("${jwt.secret}") String salt) {
        this.key = Keys.hmacShaKeyFor(salt.getBytes(StandardCharsets.UTF_8));
    }

    // 토큰 생성
    public TokenInfo generateToken(Member member) {
        // Access Token 생성
        Claims claims = Jwts.claims().setSubject(member.getEmail());
        claims.put("memberId", member.getId());

        long now = (new Date()).getTime();
        Date accessTokenExpiresIn = new Date(now + at_exp);
        String accessToken = Jwts.builder()
                .setClaims(claims)
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        // Refresh Token 생성
        String refreshToken = Jwts.builder()
                .setExpiration(new Date(now + rt_exp))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        return TokenInfo.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
    public Authentication getAuthentication(String accessToken) {
        // 토큰 복호화
        Claims claims = parseClaims(accessToken);

        // UserDetails 객체를 만들어서 Authentication 리턴
        UserDetails principal = new User(claims.getSubject(), "", Collections.emptyList());
        return new UsernamePasswordAuthenticationToken(principal, "", Collections.emptyList());
    }

    // 토큰 정보를 검증하는 메서드
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (RuntimeException e) {
            throw e;
        }

    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (RuntimeException e) {
            throw e;
        }
    }

    // Bearer 제외부분
    public String getToken(String token) {
        token = token.substring(7).trim();
        return token;
    }

    public String getEmail(String token) {
        return Jwts.parserBuilder().setSigningKey(salt.getBytes()).build().parseClaimsJws(token).getBody().getSubject();
    }

    public String resolveToken(HttpServletRequest request) {
        return request.getHeader("Authorization");
    }
}
  1. authentication.getAuthorities()를 통해 사용자의 권한 정보를 가져온다. 권한은 GrantedAuthority 객체로 표현되며, getAuthority()를 사용하여 권한 이름을 추출한다. 여러 권한이 있을 경우 쉼표로 구분하여 문자열로 합친다.
  2. 현재 시간을 기준으로 액세스 토큰의 만료 시간을 계산한다. new Date(now + ac_exp)를 통해 현재 시간으로부터 30분 후의 시간을 나타내는 accessTokenExpiresIn을 생성한다.
  3. Jwts.builder()를 사용하여 JWT 빌더를 생성한다. setSubject(authentication.getName())로 JWT의 subject(주체)를 사용자명으로 설정한다. 권한은 필요없으므로 empty로 둔다.
  4. setExpiration(accessTokenExpiresIn)으로 액세스 토큰의 만료 시간을 설정한다. signWith(key, SignatureAlgorithm.HS256)로 시크릿 키와 알고리즘을 사용하여 토큰을 서명한다. compact()를 호출하여 토큰을 문자열로 변환한다.
  5. refreshToken을 생성한다. 액세스 토큰과 동일한 방식으로 빌더를 생성하고, 만료 시간을 현재 시간으로부터 24시간 후로 설정한다.
  6. TokenInfo 객체를 생성하고, grantType을 "Bearer"로 설정하고, 생성된 액세스 토큰과 리프레시 토큰을 할당한다.

이 코드는 사용자의 인증 정보를 JWT 토큰으로 변환하여 반환하는 메서드로, 액세스 토큰과 리프레시 토큰을 생성하여 TokenInfo 객체로 래핑하여 반환한다.

JwtAuthenticationFilter

JWT 인증을 하기 위해 사용되는 필터로 JwtAuthenticationFilter를 통과하고 UsernamePasswordAuthenticationFilter를 진행하게 된다.
해당 과정은 토큰이 유효한지 검사하고 토큰 정보를 추출하는 함수이다.

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);

        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    // Request Header 에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
728x90
반응형
728x90
반응형

먼저 아래와 같은 내용으로 spring project를 하나 만들어주었다.

해당 애플리케이션을 실행하면 콘솔창에 아래와 같은 내용이 나온다. 해당 정보를 복사하여, id에는 user를 입력하고 로그인할 수 있다.

처음 애플리케이션이 실행되면 Spring Security는 Bean 중에서 type이 SecurityFilterChain인 것을 찾는다.

SecurityFilterChain을 한번 살펴보겠다.

SecurityFilterChain은 springframework.security.web의 것이고, 이것은 인터페이스이므로 스프링 bean 선언으로 이동하자.

interface 옆에 아이콘을 클릭하면 이동할 수 있다.

defaultSecurityFilterChain이라고 되어 있다. 매개변수로는 HTTPSecurity를 받고 여기서는 http.authorizeRequests()로 모든 Request는 인증되어야한다고 되어있다. 그래서 위의 화면처럼 우리는 로그인 화면에서 비밀번호를 입력해야 다른 화면으로 넘어갈 수 있는 것이다. formLogin은 Spring Security에서 기본적으로 제공해주는 로그인 화면이다. (아까 본 화면)

 

스프링으로 모든 endpoint를 자동으로 보호하기 때문에 우리가 Endpoint에 직접 접근이 불가능했던 이유이다.

(HttpBasic은 formlogin이 작동하지 않을 경우를 위해서 만든 라인이다.)

이후에 jwt 토큰을 사용하여 로그인하는 글을 포스팅해보겠다.

728x90
반응형
728x90
반응형

보안 용어

인증(authentication)

인증(authentication)은 사용자가 누구인지 확인하는 단계이고 보통 로그인을 의미한다.
사용자는 아이디와 패스워드를 입력하면 로그인은 데이터베이스에 등록되어있는지 일치 여부를 확인하고, 서버는 응답으로 사용자에게 토큰(token)을 전달한다.

인가(authorization)

인가(authorization)은 사용자가 애플리케이션 내부의 리소스에 접근할 때 사용자가 해당 리소스에 접근할 권리가 있는지를 확인하는 과정을 의미한다. 로그인한 사용자가 게시판에 접근해서 글을 보려고 하는 경우 게시판 접근 등급을 확인해서 접근을 허가하거나 거부하는 것이 인가이다.
인증 단계에서 발급받은 토큰이 인가 내용을 포함하고 있으며, 사용자가 리소스에 접근하면서 토큰을 함께 전달하는 식으로 진행하여 서버는 토큰을 통해 권한 유무 등을 확인해서 인가를 수행한다.

접근 주체(principal)

접근 주체는 애플리케이션 기능을 사용하는 주체를 의미한다. 인증과 인과 과정을 거쳐서 접근 주체에게 부여된 권한을 확인하는 과정을 거친다.

스프링 시큐리티 (Spring Security)

스프링 시큐리티도 일종의 보안 프레임워크이기 때문에 공부가 필요했다.

동작 구조

스프링 시큐리티는 서블릿 필터(Servlet Filter)를 기반으로 동작하며, 스프링이 가지는 서블릿 컨테이너인 DispatcherServlet 앞에, 필터가 배치되어 있다.

필터체인이라는 것이 Dispatcher Servlet 앞에 존재해서 클라이언트가 request를 보내면 먼저 맞닥뜨리게 된다.

필터체인

필터 체인(Filter Chain)은 웹 애플리케이션에서 요청을 처리하는 과정에서 여러 개의 필터가 순차적으로 실행되는 구조를 말한다. 필터 체인은 클라이언트로부터의 요청이 서블릿에 도달하기 전과 응답이 클라이언트에게 전달되기 전에 요청과 응답을 가공하거나 검증하는 역할을 수행한다.

필터 체인 동작 순서

클라이언트로부터의 요청이 서블릿 컨테이너에 도달하면, 요청은 필터 체인의 시작 지점인 필터 체인의 진입점으로 전달되어서 필터 체인의 첫 번째 필터부터 순차적으로 실행된다. 마지막 필터가 실행되고 나면 요청이 서블릿에 도착해서 실제 작업을 수행하고 응답도 똑같이 필터 체인의 뒤에서부터 전달된다.

스프링 시큐리티의 사용 이유

ApplicationFilterChain은 서블릿 컨테이너에서 기본적으로 제공되는 필터 체인의 구현이다. 물론, Spring Security를 사용하지 않고도 ApplicationFilterChain을 통해 보안과 관련된 인증과 인가를 처리할 수 있다. 

 보안과 관련된 인증과 인가 작업은 일반적으로 서블릿 필터를 사용하여 처리할 수 있다. 예를 들어, 사용자의 인증 정보를 확인하고 세션 관리를 수행하는 필터를 구현하여 인증 작업을 처리할 수 있다. 또한, 요청에 따라 특정 리소스에 대한 접근 권한을 확인하는 필터를 구현하여 인가 작업을 처리할 수도 있다.

 

ApplicationFilterChain은 필터 체인 내에서 필터들을 연결하여 원하는 순서로 실행할 수 있기 때문에, 보안 관련 필터들을 적절히 구성하면 보안 요구사항을 처리할 수 있지만 직접 필터를 구현하고 관리해야 한다는 점에서 Spring Security와 비교하면 개발 및 유지보수의 복잡성이 높아질 수 있기 때문에 Spring Security를 사용하게 된다. (프레임워크의 장점!)

 

Spring Security는 보안에 특화된 기능과 설정을 제공하여 보다 쉽고 강력한 보안 솔루션을 제공한다. Spring Security를 사용하면 인증, 인가, 세션 관리, CSRF 방어, XSS 방어 등의 보안 기능을 편리하게 구현하고 관리할 수 있다.

 

그래서 스프링 프레임워크에서 필터 체인을 활용하여 다양한 기능을 구현할 수 있으며, Spring Security의 SecurityFilterChain은 그 중 하나의 예이다.

 

두 FilterChain은 서로 다른 목적을 가지고 있다. ApplicationFilterChain은 서블릿 컨테이너에서 일반적인 필터 체인을 관리하고 실행하는 역할을 수행하며, SpringFilterChain은 Spring Security에서 보안 관련 필터 체인을 관리하고 실행하는 역할을 수행하게 된다.

스프링 시큐리티 동작

클라이언트가 애플리케이션에 request를 보내면 서블릿 컨테이너가 필터와 서블릿을 매칭해버린다. 스프링 시큐리티는 사용하고자 하는 필터체인을 서블릿 컨테이너의 필터 사이에서 동작시키기 위해 DelegatingFilterProxy를 사용한다.

 

DelegatingFilterProxy는 서블릿 컨테이너의 생명주기와 스프링 애플리케이션 컨텍스트 사이에서 다리 역할을 수행하는 필터 구현체이다. 표준 서블릿 필터를 구현하고 있으며, 역할을 위임할 필터체인 프록시를 내부에 가지고 있다. 필터체인 프록시는 스프링부트의 자동 설정에 의해 자동 생성된다.

DelegatingFilterProxy

DelegatingFilterProxy는 Spring Framework에서 제공하는 필터의 대리자(Delegate) 역할을 수행하는 클래스이다.

 

일반적으로 서블릿 필터(Filter)는 서블릿 컨테이너에서 직접 관리된다. 근데, Spring Framework에서는 DelegatingFilterProxy를 사용하여 필터의 관리를 Spring의 ApplicationContext로 위임할 수 있다.
(위임의 이유는 스프링의 의존성 주입할 수 있고, 스프링의 여러 기능을 사용할 수 있기 때문이다)

 

DelegatingFilterProxy를 사용하면 이후에 클라이언트의 요청이 들어오면 DelegatingFilterProxy가 요청을 가로채서 등록된 필터 빈(Bean)을 찾아서 실행한다.

즉, DelegatingFilterProxy는 서블릿 필터의 대리자 역할을 수행하여 Spring ApplicationContext 내에서 등록된 필터 빈을 실행시키는 역할을 한다. 이를 통해 Spring의 의존성 주입(Dependency Injection)이 가능하며, 필터가 Spring 관리 빈으로 선언되어 다른 Spring 구성 요소와 함께 사용될 수 있게 되는 것이다. 

 

DelegatingFilterProxy는 Spring Security 필터 체인을 연결하는 데 사용된다. Spring Security에서는 보안 필터 체인을 구성하고, DelegatingFilterProxy를 통해 이를 등록하여 필터 체인을 관리하는 것이다.

필터체인 프록시는 스프링 시큐리티에서 제공하는 필터로서 보안 필터체인(SecurityFilterChain)을 통해 많은 보안 필터(Security Filter)를 사용할 수 있다. 필터체인 프록시에서 사용할 수 있는 보안 필터체인은 List 형식으로 담을 수 있게 설정되어있어서 URI 패턴에 따라 특정 보안필터 체인을 선택해서 사용하게 된다.

 

보안 필터체인에서 사용하는 필터는 여러가지가 있고, 필터마다 실행되는 순서가 다르다.
보안 필터체인은 WebSecurityConfigurerAdapter 클래스를 상속받아서 설정할 수 있다. 필터체인 프록시는 여러 보안 필터체인을 가질 수 있고, 여러 개의 보안 필터체인을 만들기 위해서는 WebSecurityConfigurerAdapter 클래스를 상속받는 클래스를 여러 개 생성하면 된다.

 

스프링 시큐리티에서는 디폴트로는 SecurityFilterChain에서 사용하는 필터 중 UsernamePasswordAuthenticationFilter를 통해 인증을 처리한다.

 

SecurityFilterChain
SecurityFilterChain은 보안 필터의 체인이다. Spring Security는 필터 기반 아키텍처를 사용하여 요청에 대한 인증 및 인가 작업을 처리한다. SecurityFilterChain은 다양한 필터를 포함하고 있으며, 각 필터는 특정한 보안 작업을 수행한다.

예를 들어, UsernamePasswordAuthenticationFilter는 사용자 이름/암호 인증을 처리하고, AccessDecisionManager는 권한 검사를 수행한다.

 

이제 개념적인 부분을 공부해보았으니 직접 구현을 하면서 로그인, 회원가입에 적용해봐야겠다. 

728x90
반응형
728x90
반응형

일전에 EC2를 사용해서 Spring Boot 프로젝트를 배포해본 적이 있었다.

이번에 참여하게된 JSCODE 게시판 서비스를 고도화해나가는 프로젝트에서 멘토님이 Elastic BeanStalk를 사용하여 배포하라는 요구사항을 주어서, Elastic BeanStalk를 처음 사용해보았다.

EC2를 통해서 배포하였을 때는 운영체제가 설치되어 있는(linux) 컴퓨터 하나를 터미널로 다루는 느낌이었다. java 설치부터 docker나 필요한 기술을 설치하고 jar파일을 scp나 프로그램을 통해 EC2 서버로 가져와서 실행하면 잘 되었다.

Elastic BeanStalk를 처음 써봐서 그런지 더 쉽게 사용하는 Heroku같은 느낌의 서비스라고 하는데 실제로는 고생을 좀 겪었다. (EC2도 돌이켜보면 훨씬 더 많이 겪었던 것 같긴하다.. )

다시 볼 때는 이것만 보면 되게 정리해보겠다.(제발)

업로드 한 뒤에 생기는 문제들

502 Bad Gateway 문제

  1. 첫번째 해결방법

이 문제는 애플리케이션 로드 밸런서가 기본적으로 EC2 인스턴스에서 nginx 서버의 port 80을 가리키기 때문에 발생한다. nginx는 기본적으로 포트 5000으로 요청을 전달하도록 구성되어 있지만, 애플리케이션 서버는 포트 8080에서 실행된다.

  • 이 문제는 PORT라는 이름의 환경 속성을 사용하여 해결할 수 있으며 8080 구성 > 환경 속성으로 이동하여 속성을 추가한다

https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/java-se-nginx.html

[Configuring the reverse proxy - AWS Elastic Beanstalk

The port that your application listens on doesn't affect the port that the nginx server listens to receive requests from the load balancer.

docs.aws.amazon.com](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/java-se-nginx.html)

  • 이렇게 하던가, 아니면 application.yml에서 port를 5000번으로 열리도록 하면 된다. 아래 내용만 yml에 추가해주면 된다.이렇게 해서 해결되면 성공이다.
  • server: port: 5000

5000번으로 port를 열었는데, local에서 실행이 안되는 경우!*
5000번으로 열고 확인할려고 local에서 실행했는데, 안되는 경우가 있을 것이다. port가 이미 사용중이라고 뜨는 경우였는데, kill -9 명령어로 계속 죽여도 다시 살아난다(ㄷㄷ)
나의 경우에는 mac에서 5000포트를 airplay를 사용하는 경우 때문에 생겼었다. 설정에 들어가서 해당 airplay를 꺼두면 local에서 실행이 된다.
물론, 배포할때는 아무 문제 없기 때문에 local에서 5000번 포트로 돌릴때만 문제가 된다.
https://jaynamm.tistory.com/entry/%EB%A7%A5%EB%B6%81%EC%97%90%EC%84%9C-5000%EB%B2%88-%ED%8F%AC%ED%8A%B8%EA%B0%80-%EC%82%AC%EC%9A%A9%EC%A4%91%EC%9D%B4%EB%8B%A4-MacOS-Montrey

이렇게 해도 계속 오류인 경우 -> log를 살펴보자

나는 이렇게 해서 업로드가 되었지만 warning이 계속 뜨는 오류가 발생하였다.

해당 오류를 확인하기 위해서 log를 확인하였다.

No Active Profile Set

: 실제 502 Gateway의 오류가 나왔을 때, 로그에서 확인한 오류이다. Spring은 잘 실행되는데, Spring에서 오류가 생겼다.

그럼 서버는 제대로 실행하려고 하는데 서비스의 오류일 가능성이 높다. 서비스에서 오류가 생길만한 건 데이터베이스 서버밖에 없었다. (나머지는 local에서 잘 실행되므로..)

나의 경우는 데이터베이스 연결의 문제였다. 이때는 하나하나씩 차례대로 진행하여서 로컬에서 먼저 데이터베이스의 엔드포인트를 통해서 접속이 되는지 확인해야 한다.

데이터베이스 보안 그룹 열어주기!

AWS EC2(Elastic BeanStalk도 결국 EC2 생성해주는 것이다!)에서 보안 그룹 규칙을 통해서 EC2 인스턴스로 들어오고 나가는 트래픽을 세부적으로 제어할 수 있도록 해준다. 예를 들어 80번 포트는 모든 사람들이 접근할 수 있도록 열어주고 SSH 통해 접속하는 22번은 나만 접근하도록 허용할 수도 있다.
VPC
VPC는 하나의 네트워크 그룹이다.

인바운드 규칙
인바운드란 외부에서 인스턴스로 들어오는 요청을 의미한다. 유형을 선택하면 프로토콜과 포트가 자동으로 선택이 된다.

아웃바운드 규칙
아웃바운드는 인스턴스에서 외부로 나가는 트래픽인데, 디폴트 값이 모두 허용이기 때문에 건들지 않았다.

데이터베이스 설정을 위해서는 인바운드 규칙을 수정했어야 한다. MySQL 서버가 접속 되도록 해야하기 때문이다.
인바운드 규칙 편집에 들어가서 유형 중 MYSQL/AURORA를 선택하면 프로토콜과 포트가 TCP, 3306으로 자동 설정된다.
그리고 소스 유형을 AnyWhere IPv4를 선택하여 규칙을 저장한다. 그럼 데이터베이스 보안 그룹 설정이 완료된다.

어떤게 데이터베이스 endpoint?

여기서 문제가 어떤게 해당 Elastic BeanStalk랑 연결된 데이터베이스인지가 의문이였다. 먼저 처음 Elastic BeanStalk 환경 구성을 설정해줄때 데이터베이스를 추가했을 것이다.

해당 페이지의 환경에 들어가면 아래와 같은 화면이 있다. 해당 내용이 존재하면 RDS 창에 들어가면 데이터베이스가 생성되어 있다는 의미이다. 거기서 endpoint를 찾아야 한다! RDS를 AWS 검색창에 검색해서 들어가자.

그럼 아래와 같은 창 중에서 elastic bean stalk에서 만든 이름과 유사한 DB 식별자가 있다!(인스턴스 확인하면서 맞는지 체크한다)

해당 DB식별자 안으로 들어가면 엔드포인트가 나와있다. 해당 엔드포인트를 통해서 로컬에서 돌아가는지 먼저 확인해보았다.

나는 MySQL을 사용하기 때문에, workbench에서 확인하였다. +버튼을 클릭하여 접속한다.

그리고 Connection Name은 그냥 앱 이름, Hostname은 엔드 포인트를 입력하면 되고, password는 우리가 Elastic BeanStalk에 설정한 비밀번호를 입력해주면 된다!

그래서 잘 연결된다면, DB설정이 잘된 것이다. 해당 DB 엔드포인트를 yml파일에 설정해주고 jar로 배포해주면 된다!

(만약 안된다면, DB가 연결이 안된 것이므로 Elastic BeanStalk에 들어가 DB를 다시 확인해주어야 한다.)

이제 위에서 뜬 No Active Profile Set 오류는 뜨지 않았다. database가 없다는 오류가 떠서, 그냥 해당 workbench에서 board라는 db를 만들어주었다!

(해결)
일단 보안그룹 설정에 대해서 확실히 몰랐던 것이 컸던 것 같다. 인바운드 규칙과 아웃바운드 설정을 먼저 살펴봐야한다.
또, ElasticBeanStalk는 새로운 서비스라기 보다는 EC2에서 하던 것 그대로인데 더 편리하게 설정해 주는 거라고 생각하면 될 것 같다.
똑같이 EC2 인스턴스가 생기고 RDS가 생기므로, 결국 EC2에 대한 설정과 동일하다.
Elastic BeanStalk는 단일 서비스일때는 유용하게 사용할 수 있을 것 같다.

EC2와 Elastic BeanStalk의 차이

  1. 관리 수준:
    • EC2: Amazon EC2는 InfraStructure 서비스로, 가상 서버를 프로비저닝(운영체제 선택, 네트워크, 보안 등등의 설정)하고 관리해야 한다. 서버의 운영 체제 선택, 네트워크 구성, 보안 설정, 스케일링 등을 직접 관리해야 한다.
    • Elastic Beanstalk: Elastic Beanstalk은 관리형 플랫폼 서비스로, 애플리케이션 배포와 관리를 단순화한다. 코드를 업로드하면 Elastic Beanstalk이 인프라와 런타임 환경을 자동으로 프로비저닝하고 애플리케이션을 배포한다. 서버 인스턴스, 로드 밸런서, 스케일링 등의 관리 작업은 Elastic Beanstalk이 자동으로 처리한다.
  2. 배포 방식:
    • EC2: EC2는 단일 가상 서버 또는 가상 서버 그룹으로 애플리케이션을 배포한다. 서버 인스턴스를 직접 프로비저닝하고 로드 밸런서를 구성하여 트래픽을 분산시켜야 한다.
    • Elastic Beanstalk: Elastic Beanstalk은 애플리케이션을 여러 개의 EC2 인스턴스로 배포한다. Elastic Beanstalk이 자동으로 로드 밸런싱, 자동 스케일링, 로깅 및 모니터링을 처리하므로 개발자는 배포에 집중할 수 있다.
  3. 확장성:
    • EC2: EC2는 가상 서버 인스턴스를 수동으로 확장하거나 축소해야 한다. 서버 수준에서 수평 스케일링이 이루어지며, 필요한 만큼의 가상 서버를 추가하거나 제거하여 처리량을 조정할 수 있다.
    • Elastic Beanstalk: Elastic Beanstalk은 애플리케이션 수준에서 자동으로 확장된다. 트래픽이 증가하면 Elastic Beanstalk이 필요한 수의 EC2 인스턴스를 자동으로 생성하고 관리한다. 이로 인해 애플리케이션의 확장성이 향상된다.

Elastic BeanStalk를 사용했을 때 편리했던 점은 업로드 할 때 따로 프로그램을 사용하거나 리눅스 명령어 필요없이 jar 파일을 바로 업로드하면 된다는 점이다. 또, EC2가 바로 생기고 데이터베이스가 바로 연결된다.(물론 보안 그룹 설정을 못해서 한참 고생하였다)

아래는 멘토님께서 알려주신 방법이다. 하나하나 차례대로 확인해보면 좋을 것 같다. (덕분에 너무 잘 해결할 수 있었던 것 같다)

Elastic Beanstalk가 제대로 실행되지 않아서 디버깅을 해야 할 때 / Elastic Beanstalk 디버깅 방법

1. 이벤트 확인하기

2. 로그 확인하기

여기서 web.stdout.log를 확인하면 된다!

3. EC2에 직접 들어가서 서버 실행시켜보기

여기서 아래에 연결 부분을 복사하여 접속하면 된다.

$ sudo su # root 권한으로 변경하기
$ cd /var/app/current # EB를 통해 업로드한 파일이 있는 디렉토리이다.

해당 위치에서 직접 명령어를 하나하나 쳐보면서 잘 작동하는지 디버깅 해보고, 여기서 디버깅 해봄으로써 에러 코드를 바로바로 직접 확인할 수 있어서 어디에 버그가 존재하는지 쉽게 찾을 수 있다.

728x90
반응형

'백엔드' 카테고리의 다른 글

GithubAction을 이용해서 Spring 프로젝트 CI/CD(with Docker)  (0) 2023.09.06
세션  (0) 2023.08.25
[Spring] Spring으로 웹 개발을 진행할 때 알아야할 정보  (0) 2023.05.23
Gradle  (0) 2022.12.27
Amazon S3  (0) 2022.07.12
728x90
반응형

웹은 기본적으로 클라이언트가 요청하면 서버에서 데이터를 내려주는 식으로 동작한다. 여러 방식에 따라 어떤 언어와 프레임워크를 선택할지 다르다.
클라이언트(브라우저)가 서버에서 html, css, js 모든 것을 받아서 보여줄 수도 있고, html, css를 서버에서 받은 js를 이용하여 보여줄 수도 있다.
그리고 동적인 정보, 정적인 정보 어떤 정보를 받느냐에 따라서 달라질 수 있다.

간단한 웹 서비스를 개발하려면 동적인 것이 필요없을 수 있다. 그냥 html, css, js를 서버로부터 받아와서 보여주기만 하면 되고, 클라이언트가 클릭하거나 하는 동적인 활동이 필요없는 웹 화면일 수 있다. 하지만 웹 서비스에 동적인 리소스가 필요없을 수가 없다.

처음에는 WAS 하나로 모든 것을 해결해주었다. 서버에서 정적인 파일인든, 동적인 파일이든 애플리케이션 로직과 분리도 되어있지 않고 모든 것을 포함하는 것이다. 그치만 이렇게 하면 WAS가 너무 많은 역할을 담당하게 된다. 그래서 WAS를 나눈다.

웹 애플리케이션 서버(WAS)

WAS는 제일 큰 개념이라고 생각하면 된다.

WAS는 Web Server랑 서블릿 컨테이너 그리고 데이터베이스 연결 등등이 있지만, Web Server와 서블릿 컨테이너를 위주로 보겠다.

Web Server

웹 서버를 정적인 리소스를 이용하여 전달해주는 것이다. 웹 서버의 예시로는 Apache HTTP Server나 NginX같은 것이 있다.

서블릿 컨테이너

서블릿 컨테이너는 서블릿을 실행하기 위한 환경을 제공하고 서블릿의 생명주기를 관리한다. 그럼 서블릿이란 뭘까?

서블릿

이전에 동적인 동작을 처리하기 위해서 서블릿이 필요하다. 서블릿은 클라이언트의 요청을 담당하고 동적인 웹 콘텐츠를 생성하고 전송한다. 서블릿은 Java Servlet API에 정의되어 있는 인터페이스를 구현하여서 개발된다. 즉, WAS는 서블릿 컨테이너를 내장하고 있어서 서블릿을 실행할 수 있게 되는 것이다.

서블릿은 HTTP 요청을 받고 HTTP 응답을 JSON 형식으로 내려줄 수 있다. 서블리의 예시로는 톰캣이나 Jetty가 있다. 여기서 톰캣을 가지게 되는 서블릿 컨테이너의 이름이 Apache Tomcat이다.

Spring, Apache Tomcat

Spring Framework 자체적으로는 Apache Tomcat을 내장하고 있지 않아서 만약 Spring 애플리케이션을 실행하기 위해서는 별도로 Tomcat을 설치하고 설정해야 한다. 이 과정이 복잡하고 어렵기 때문에 Spring Boot를 프로젝트를 시작할 때 사용하게 된다.

Spring Boot 사용 이유

Spring Boot 프로젝트를 사용할 경우에는 내장 서블릿 컨테이너로 Tomcat, Jetty, Undertow 중 하나를 선택하여 사용할 수 있다. Spring Boot는 웹 애플리케이션을 실행하는 데 필요한 내장 서버를 자동으로 제공하므로 별도의 서버 설정이 필요하지 않다.

Spring Framework는 서블릿 컨테이너와 통합되어 웹 애플리케이션 개발을 지원하지만, Spring 자체적으로 Apache Tomcat을 내장하고 있는 것은 아니고, Apache Tomcat은 Spring 애플리케이션을 실행하기 위한 선택적인 서블릿 컨테이너 중 하나이고 Spring Boot에서 자체적으로 제공한다고 생각하면 된다.

728x90
반응형

'백엔드' 카테고리의 다른 글

GithubAction을 이용해서 Spring 프로젝트 CI/CD(with Docker)  (0) 2023.09.06
세션  (0) 2023.08.25
[AWS] Elastic BeanStalk 배포와 Trouble Shooting  (0) 2023.05.23
Gradle  (0) 2022.12.27
Amazon S3  (0) 2022.07.12
728x90
반응형

Java 컬렉션 (Collection) 정리

Java Collection은 Java 언어에서 데이터를 저장, 처리, 관리하는 데 사용되는 API 모음이다.

특히나 자료구조에 대해서 이해한다면 쉽게 이해해서 사용할 수 있다.

객체들을 효율적으로 추가, 삭제, 검색할 수 있도록 java.util 페키지에 collection과 관련된 인터페이스와 클래스를 포함시켰기 때문에 사용할 수 있다.

(매번 찾아보는 것보다 한 번 정리해야겠다는 생각이 들어서 쓰게 되었다.)

데이터 구조엔 여러가지 종류들이 있다 : ArrayList, LinkedList, Hashtable, Tree ...

java가 제공하는 집합 인터페이스 : List, Set, Queue, Map 그리고 각각의 구현체들



List

  • 순서 있는 집합 → 어느 요소가 어디에 삽입되는지 제어가능!
  • 중복된 요소들을 허용
  • 어느 위치에 어떤 객체가 있는지를 신경씀
List<String> words = List.of("Apple", "Bat", "Cat"); // of는 정적 메소드
words.size(); // 3
words.isEmpty(); // false
words.get(0); // Apple
words.contains("Dog"); // false
words.indexOf("Cat"); // 2

of 메소드
: Java 9부터 추가된 List 인터페이스의 메소드. 주어진 요소들을 포함하는 불변리스트를 생성함.
리스트 생성하고 초기화할 때 사용하면 좋다. 그리고 null 요소도 허용하지 않는다.
List.of로 만든 instance는 수정할 수 없다. (불변 리스트)

immutable List
불변 클래스란 무엇일까? -> String, BigDecimal, Wrapper 클래스는 불변이다.
이 의미는 특정 클래스의 인스턴스를 만든 순간부터 이 값을 바꿀 수 없게 된다는 것이다.

가변 리스트를 만들고 싶으면 ArrayList나 LinkedList, Vector를 만들어야 한다.

List<String> wordsArrayList = new ArrayList<String>(words);
List<String> wordsLinkedList = new LinkedList<String>(words);
List<String> wordsVector = new Vector<String>(words);

ArrayList와 LinkedList의 차이

  • ArrayList는 접근은 빠르지만 삽입 삭제가 느리다. 메모리가 연속적
  • LinkedList는 접근은 느리지만 삽입 삭제가 빠르다. 메모리가 따로따로. 서로 연결되는 메모리 주소를 기억하고 있다.
  • java에서의 LinkedList는 요소들이 양면으로 연결되어 있다. forward로 향하는 연결과 backward로 향하는 연결이 둘 다 존재한다. (doubly linked)
  • 변경점이 적다면 ArrayList, 삽입과 제거가 빈번하다면 LinkedList를 사용하면 된다.

Array : 배열은 모든 요소들을 순서대로 저장하고 언제나 요소 하나를 제거하면 그 요소를 제거한 후 그 다음 요소를 왼쪽으로 밀어야 한다. 하지만, 배열의 특정한 위치에 있는 요소를 갖고 오는 건 쉽다.

LinkedList : 각자 요소들은 다음 요소들과 연결되어 있다. 삽입과 삭제가 더 쉽다. 하지만 특정 위치에 있는 요소를 갖고 오거나 찾는 것은 연결점들을 다 건너 다니면 무엇이 있는지 찾아야 한다.


Vector랑 ArrayList는 언제 구분해서 써야할까?

Vecotr 클래스는 자바1때부터 있었다. ArrayList는 자바1.2부터.(오래 전부터 있었다는 뜻)

그럼 Vector의 문제가 무엇이여서 ArrayList가 나타난 것일까?

  • vector는 모든 메서드들이 synchronize가 되어있는데, ArrayList는 Synchronize되어 있지 않다.
  • vector는 내부의 'add', 'remove', 'set'과 같은 변경 작업을 할 때 내부적으로 synchronized를 사용한다.
  • Synchronize 키워드는 어떤 차이를 만들까?
    • 예를 들어서 한 클래스 안에 25개의 동기화된 메소드들이 있다고 하자.
    • 만약 이 Vector 클래스의 인스턴스가 여러 스레드 사이에 공유된다면, 스레드 중 단 하나만이 이 25개의 메소드들을 실행할 수 있다.
    • 즉, 이 동기화된 메소드들 안에서는 한순간에 오직 하나의 스레드만 코드를 실행시킬 수 있다.
    • 이렇게 하는 이유는 프로그램이 안전하길 바라기 때문이다.
    • 우리의 프로그램은 쓰레드 하나가 사용하든 15개의 쓰레드가 사용하든 행동방식이 바뀌면 안되고, Synchronized가 그 역할을 하려 하는 것이다.
    • Vector는 Thread-safe하다. 여러 쓰레드들 사이에서 데이터를 공유하는 상황에서 Vector를 사용할 수 있다.
    • 그러나 ArrayList는 안전하지 않다. 그치만, 보안은 언제나 성능에 타격을 준다.(보안 상승, 성능 하락 / 보안 하락, 성능 향상)
    • 왜냐하면 스레드 하나가 동기화된 메소드를 실행 중일때 그럼 다른 스레드들은 그 스레드가 동기화된 메소드의 실행을 완료할때까지 기다릴 수 있기 때문이다.
    • 안전이 필요하지 않는 이상 ArrayList가 더 낫다.
    • 동기화는 기초적인 안전 구현방식 중 하나이다.
      • 그래서 만약 멀티 쓰레드 환경이 아닐 때 Vector 클래스를 사용하게 되면 성능이 떨어지게 된다.
    • 이를 위해, ArrayList를 위해 Collections 클래스에서는 synchronizedList 메소드를 제공한다.
    • ArrayList클래스를 멀티스레드 환경에서 사용해야 한다면 CopyOnWriteArrayList 또는 Collections.synchronizedList를 사용해야 한다.

반복문

java Collection 인터페이스의 Iterator는 Collection 요소를 하나씩 반복적으로 접근할 수 있다.

  • hasNext() : 다음 요소가 있는지 true, false를 반환함
  • next() : 다음 요소 반환
    for(int i=0;i<words.size();i++){
      System.out.println(words.get(i));
    }
    

for(String word:words){
System.out.println(words.get(i));
}

// Iterator 사용하기
Iterator wordsIterator = words.iterator();
while(wordsIterator.hasNext()){
System.out.println(wordsIterator.next());
}


그럼 3가지 중 어떤 루프를 써야할까?

```java
while(wordsIterator.hasNext()){
    System.out.println(wordsIterator.next());
}
List<String> words = List.of("Apple","Bat","Cat");
List<String> wordsAl = new ArrayList<>(words);
for(String word:words){
    if(word.endsWith("at"))
        System.out.println("word = " + word);
}

밑의 방법은 추천하지 않는다.

for(String word:wordsAl){
    if(word.endsWith("at")){
        wordsAl.remove(word);
    }
}

해당 방법은 개선된 loop의 중간에서 변경점을 만들면 단어를 제거함으로써 반복이 어떻게 진행되는지가 바뀔 수가 있기 때문에, 반복자를 사용하는 것이 좋다.

Iterator<String> iterator = wordsAl.iterator();
while (iterator.hasNext()) {
    if(iterator.next().endsWith("at")){
        iterator.remove();
    }
}

List 안에는 primitive type를 포함할 수 없다. List 인터페이스는 제네릭 타입을 지원하기 때문에, List 내부에는 오직 객체만 포함시킬 수 있다.
그래서 사용하려면 primitive type을 객체로 래핑한 Wrapper 클래스를 사용하여 포함해야 한다.

AutoBoxing : primitive 타입의 값을 해당하는 wrapper 클래스의 객체로 바꾸는 과정을 의미한다.

java 컴파일러는 primitive type이 아래 두 가지 경우에 해당될 때 autoBoxing을 적용한다.

  1. primitive type이 Wrapper 클래스의 타입의 파라미터를 받는 메서드를 통과할 때
  2. primitive type이 Wrapper 클래스의 변수로 할당될 때

List를 만들려 할 때 일어나는 일은 다 AutoBoxing되어 Wrapper Class가 생성되는 것이다.


sort static solution
Collections.sort는 static method이다. (즉, Collection을 따로 생성자를 통해 생성하지 않아도 사용할 수 있다. )

List는 Collection Interface를 연장한다. 즉, 이것은 Collection Interface에 있는 모든 것을 구현하고, 객체의 위치에 상관하는 메서드를 제공한다.


정리 : List interface의 구현

→ ArrayList : 배열을 기초적 데이터 구조로 사용. LinkedList에 비해서 삽입과 삭제가 느리다.

but, 특정 위치의 특정 요소에 접근하고 싶다면 매우 빨리 수행할 수 있다.

→ LinkedList : 특정 위치의 요소 찾는 것은 느리지만, 요소의 삽입과 제거는 훨씬 빠르다

→ Vector : 다중 스레드 시나리오에서는 성능 하락.



Set

  • 용도 : 중복이 허용되지 않는 집합을 만들 때 사용한다.
    Unique things only : 중복이 허용되지 않는다!
  • List interface와 비교했을 때, Set interface는 위치 접근을 허용하지 않는다. Set은 그냥 랜덤위치에 저장한다. (갖고 있다는 사실이 중요)
  • Set은 기본적으로 변경을 허용하지 않는다. 따라서, 다음 코드는 되지 않는다.
Set<String> set = Set.of("Apple","Banana","Cat");
set.add("Apple"); // 오류남. -> 왜냐하면 변경을 허용하지 않기 때문
  • HashSet : Set의 여러 구현 중 하나.
  • 안에 요소들이 있는지 없는지는 신경쓰지만, 위치는 신경쓰지 않는다.
set.add(2,"Apple");
  • 특정 위치에 요소를 추가할 수 없다. Set은 유일한 값들을 저장하는데 사용된다.


Queue

작업하고 싶은 순서대로 정렬할 때 사용된다.

컬렉션의 메서드를 모두 지원한다.

우선순위 Queue : 지정한 순서대로 정렬되어 있다.

물론 Comparator로 순서를 지정할 수 있다.

Queue<String> queue = new PriorityQueue<>();
queue.poll(); // 아무것도 없어서 안빠짐
queue.offer("Apple");
queue.add("Banana");
System.out.println("queue = " + queue); // queue = [Apple, Banana]

queue.addAll(List.of("Zebra", "Sero"));
System.out.println("queue = " + queue); // queue = [Apple, Banana, Zebra, Sero]

System.out.println(queue.poll()); // Apple
System.out.println(queue.poll()); // Banana
System.out.println(queue.poll()); // Sero
System.out.println(queue.poll()); // Zebra
System.out.println(queue.poll()); // null

우선순위 대로 poll해서 뺐을 때, 나오는 걸 볼 수 있다.

Queue<String> queue = new PriorityQueue<>();
queue.addAll(List.of("Zebra", "Monkey", "Cat"));
System.out.println(queue.poll()); // Cat
System.out.println(queue.poll()); // Monkey
System.out.println(queue.poll()); // Zebra

Comparator 지정하기 -> 이 코드에서는 길이가 짧은 순으로 나오게 할 수 있다.

static class StringLengthComparator implements Comparator<String> {

    @Override
    public int compare(String o1, String o2) {
        return Integer.compare(o1.length(), o2.length());
    }
}

public static void main(String[] args) {

    Queue<String> queue = new PriorityQueue<>(new StringLengthComparator());
    queue.addAll(List.of("Zebra", "Monkey", "Cat"));
    System.out.println(queue.poll()); // Cat
    System.out.println(queue.poll()); // Zebra
    System.out.println(queue.poll()); // Monkey
}


Map

→ Map은 Collection interface를 연장하지 않는다! Map은 Collection 프레임워크의 일부이지만 Collection 인터페이스를 구현하지는 않는다!

키-쌍을 저장하는데 사용한다.

HashMap(정렬되지 않고, 순서없음)

  • HashMap과 Hashtable의 차이 : 둘 다 해싱 기법을 사용했다는 점에서는 동일하다Hashtable(HashMap과 같지만, 모든 메서드가 동기화되어 있어 스레드가 안전)
  • Hashtable은 Vector와 비슷하다 → 동기화되어 있음. Hashtable의 모든 메소드는 동기화 되어 있다.
    • 스레드가 HashMap에 비해 더 안전하고, HashMap과 동일하게 Hashtable도 분류되어 있지도 않고 순서가 있지도 않다.
  • Hashmap은 열쇠를 null 값과 저장할 수 있게 해준다. → Hashmap 안에는 key와 null값을 저장할 수 있다. (Hashtable에서는 할 수 없다.)LinkedHashMap(삽입 순서 기억)
  • LinkedHashset과 비슷하게 순서가 유지된다. 그냥 삽입 순서대로 들어간 것이기 때문에 HashMap에 비해서는 삽입과 제거가 느리다. 하지만 요소간에 연결이 되어있어 요소를 도는 이터레이션은 훨씬 빠르다.TreeMap(데이터를 정렬된 순서로 저장)
  • 기반 데이터 구조는 Tree, 정렬된 순서로 저장된다.
  • 트리가 존재할 때는, 데이터가 정렬되어 있기 때문에, 다른 인터페이스(NavigableMap)도 구현한다.

Hashtable

배열과 비슷하게 고정된 위치들과, LinkedList의 장점을 합친 것이다. 각각의 위치를 양동이라고 하면 양동이에 여러 가지를 저장할 수 있는 것이다.
양동이에 차곡차곡 여러 가지를 쌓을 수 있는 것이다.
그럼 어떤 양동이에 저장할지 결정하느냐? 이때 나오는 것이 해싱 함수를 사용한다.
만약 배열의 크기가 13이라고 생각해보자. (index는 그럼 0부터 12까지 있는 것이다.)

만약 15를 어디에 저장해야 할지 결정한다면 어느 양동이에 15를 저장해야 할지 어떻게 정할까? 여기에 13개의 양동이가 있으므로, 15를 13으로 나누고 나머지를 구해서 양동이에 요소를 넣는 것이다.

15는 그럼 인덱스 2의 양동이에 들어가게 되는 것이다.

즉, 해싱 함수는 어느 양동이에 요소가 들어갈지 정해주는데 사용된다.

2를 삽입한다면 2번 자리에 저장하고 싶을 것이니까 아까 저장한 15위에 2를 붙이면 된다.

34를 지우고 싶으면 8인덱스에 와서 요소를 지운다. (34나누기 13의 나머지는 8이니까!)

HashTable의 장점은 요소들을 쉽게 삽입할 수 있고, 검색과 제거 또한 훨씬 쉽게 할 수 있다.

HashTable은 매우 빠른 검색능력을 제공한다. 요소의 삽입은 때때로는 LinkedList보다 느릴 수 있지만, 배열에 비해서는 훨씬 빠르다.
HashTable의 효율성은 언제나 해싱 함수의 효율성에 기반한다.(위에 예시로 든 건 그냥 설명을 위해서이지 해싱함수마다 다르다.)

Java에서는 해싱함수를 해시코드란 것을 이용하여 구현한다. 객체 클래스를 보면, hashcode()라는 메서드가 있다.

hashcode는 어느 양동이에 객체가 저장되는지를 결정하는데 사용된다. 위에서 얘기하였던 나머지를 이용해서 저장하는 것은 그냥 예시일 뿐이다. 해싱 함수들은 hashcode()를 이용해서 java에서 구현할 수
있다.

Map<String, Integer> map = Map.of("A", 3, "B", 5, "z", 10);
// map.put("R", 1); of를 써서 삽입할 수 없음
System.out.println(map.get("z"));
System.out.println(map.size());
System.out.println(map.containsKey("A"));
System.out.println(map.containsValue(3));
System.out.println(map.keySet());
System.out.println(map.values());

/*
10
3
true
true
[z, A, B]
[10, 3, 5]
*/
Map<String, Integer> hashmap = new HashMap<>(map);
hashmap.put("F", 5);
System.out.println(hashmap);
hashmap.put("z", 11);
System.out.println(hashmap);

/*
{A=3, z=10, B=5, F=5}
{A=3, z=11, B=5, F=5}
*/

HashMap, LinkedHashMap, TreeMap 서로 비교

Map<String, Integer> hashmap = new HashMap<>();
hashmap.put("Z", 5);
hashmap.put("A", 15);
hashmap.put("F", 25);
hashmap.put("L", 250);
System.out.println("hashmap = " + hashmap); // hashmap = {A=15, F=25, Z=5, L=250} -> 순서 맘대로

LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("Z", 5);
linkedHashMap.put("A", 15);
linkedHashMap.put("F", 25);
linkedHashMap.put("L", 250);
System.out.println("linkedHashMap = " + linkedHashMap); // linkedHashMap = {Z=5, A=15, F=25, L=250} -> 순서 유지

TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put("Z", 5);
treeMap.put("A", 15);
treeMap.put("F", 25);
treeMap.put("L", 250);
System.out.println("treeMap = " + treeMap); // treeMap = {A=15, F=25, L=250, Z=5} -> 정렬해서 가짐

어떤 문장이 주어졌을 때 나오는 Character와 단어 수 구하기

String temp = "This is a great thing! This has never happened before.";
Map<Character, Integer> occurances = new HashMap<>();

char[] chars = temp.toCharArray();

for (char character : chars) {
    Integer integer = occurances.get(character);
    if (integer == null) {
        occurances.put(character, 1);
    }else{
        occurances.put(character, integer + 1);
    }
}
System.out.println("occurances = " + occurances);

// occurances = { =9, a=4, !=1, b=1, d=1, e=7, f=1, g=2, h=5, i=4, n=3, .=1, o=1, p=2, r=3, s=4, T=2, t=2, v=1}
String temp = "This is a great thing! This ahs never happened before.";
Map<String, Integer> stringOccurances = new HashMap<>();
String[] words = temp.split(" ");

char[] chars = temp.toCharArray();

for (String word : words) {
    Integer integer = stringOccurances.get(word);
    if (integer == null) {
        stringOccurances.put(word, 1);
    }else{
        stringOccurances.put(word, integer + 1);
    }
}
System.out.println("stringOccurances = " + stringOccurances);

// stringOccurances = {a=1, never=1, before.=1, This=2, is=1, great=1, ahs=1, thing!=1, happened=1}


정리

Hash → 이걸 보게 되면 순서도 없고 정렬도 되어있지 않다.

Linked → 요소들이 서로 연결되어 있다. 순서는 확실히 유지된다. 정렬된 방식으로 저장하지는 않는다.

Tree→ 데이터가 트리 구조에 정렬된 상태로 저장된다. 기본적으로 정렬되어 있기 때문에 여러 메서드 사용할 수 있다.

728x90
반응형

'백엔드 > Java' 카테고리의 다른 글

[Java] 추상클래스 vs 인터페이스  (0) 2023.03.23
oop의 특징 (캡슐화, 상속, 추상화, 다형성)  (0) 2023.03.17
Array, ArrayList  (0) 2023.03.17
Java 실행 원리와 JVM  (0) 2022.11.13
static keyword 정적 키워드  (0) 2022.11.06
728x90
반응형

추상클래스

추상 클래스는 공통되는 부분을 모아서 추상 클래스에 정의하고, 그 외의 부분을 자식 클래스에서 확장하여 사용하는 개념으로 보면 된다.

예를 들어서, 내가 조교를 하면서 교수님께 메일을 보내는 경우이다.

나는 항상 “교수님, 안녕하십니까?”를 메일 제일 처음에 붙이고, 제일 마지막에 “항상 좋은 강의 감사합니다.” 를 붙인다. 그리고 중간의 내용은 언제나 바뀔 수 있다. 중간의 내용은 추상 메서드로 만든다.

public abstract void 교수님께메일 {
    public void 머릿말(){
        System.out.println("교수님, 안녕하십니까?");
    }
    public abstract void 내용();
    public void 맺음말(){
        System.out.println("항상 좋은 강의 감사합니다.");
    }
}

그럼 ‘교수님께 메일’ 이라는 abstract class를 만들고, ‘추가 수강 신청’과 “강의 질문” class는 abstract를 구현한 클래스이다.

public class 추가_수강_신청 extends 교수님께메일{
    @Override
    public void 내용() {
        System.out.println("혹시 이번 강의 추가신청 가능할까요?");
    }
}

public class 강의_질문 extends 교수님께메일{
    @Override
    public void 내용() {
        System.out.println("혹시 이번 강의 질문드려도 될까요?");
    }
}

그럼 이런 클래스를 활용하려면 객체를 만들어서 ‘머릿말’, ‘내용’, ‘맺음말’을 사용하면 된다.

추가_수강_신청 pm = new 추가_수강_신청();
pm.머릿말();
pm.내용();
pm.맺음말();

강의_질문 pm2 = new 강의_질문();
pm2.머릿말();
pm2.내용();
pm2.맺음말();

추상 클래스는 위의 예에서 보듯이, 필수적으로 구현해야 하는 기능을 공통적으로 묶고, 만약 그 안에 구현해야 되는 내용이 있고 내용이 조금 다르다면, 해당 내용을 추상 메서드로 선언하여 구현하면 된다.

인터페이스

그럼 인터페이스는 언제 사용할까? 대상이 만약 교수님이 아니라, 다른 모든 사람들에게 보낸다고 생각했을때, 그때도 머릿말, 내용, 맺음말은 필요하다. 그러나 해당 대상은 교수님이 아니라 형식만 interface로 선언해주고, 구현 내용은 다르게 해주는 것이다.

public interface 메일형식 {
    void 머릿말();
    void 내용();
    void 맺음말();
}
public class 친구에게 implements 메일형식{
    @Override
    public void 머릿말(){
        System.out.println("ㅎㅇ");
    }
    @Override
    public void 내용(){
        System.out.println("낼 머함");
    }
    @Override
    public void 맺음말(){
        System.out.println("");
    }

}

public class 교수님에게 implements 메일형식{
    @Override
    public void 머릿말(){
        System.out.println("교수님 안녕하십니까?");
    }
    @Override
    public void 내용(){
        System.out.println("낼 미팅 날짜 잡아도 되겠습니까?");
    }
    @Override
    public void 맺음말(){
        System.out.println("감사합니다.");
    }

}

메일은 동일한 목적을 가지는 머릿말, 내용, 맺음말이 동작이 준비되지만 저마다의 방식으로 가능하다.

또한, 동물 예시도 들고왔다.

public abstract class Animal {
    public abstract void eat();
    public void sleep() {
            // 일반 메소드 동작 정의
    }
}

그런 다음에 이 Animal 클래스를 확장하여 개, 고양이, 물고기 등으로 확장하면, 각 동물들이 먹는 기능을 이렇게 따로 정의할 수 있다.

public class Dog extends Animal {
    public void eat() {
        System.out.println("개밥");
    }
}

public class Cat extends Animal {
    public void eat() {
        System.out.println("고양이밥");
    }
}

public class Fish extends Animal {
    public void eat() {
        System.out.println("물고기밥");
    }
}

기본적인 구현은 Animal 에서, 고유의 동작은 Dog, Cat, Fish 에서 정의된다.

이번에는 같은 동물인데 인터페이스를 활용할 수 있다. 간단히 소리를 내는 하나의 추상 메소드만 정의하였다.

public interface Audible {
    void makeSound();
}

그런 다음에 각 동물에 대해 소리를 내는 기능을 추가한 뒤 makeSound() 메소드를 정의하였다.

public class Dog extends Animal implements Audible {
    ...

    public void makeSound() {
        System.out.println("멍");
    }
}

public class Cat extends Animal implements Audible {
    ...

    public void makeSound() {
        System.out.println("냐옹");
    }
}

그러면 강아지와 개는 소리를 내는 기능이 더해졌고 각 클래스 내에서 어떤 소리를 내는지 정의하였습니다.

그런데 물고기는 소리를 따로 내지 않는다. 별도의 인터페이스를 만들어서 활용해 보겠다.

public interface SilentAnimal {

}

그리고 Fish 클래스에 이를 구현하도록 해볼게요.

public class Fish extends Animal implements SilentAnimal{
    public void eat() {
        System.out.println("사료");
    }
}
Animal[] animals = new Animal[3];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Fish();

for (Animal animal : animals) {
    if (animal instanceof Audible) {
        ((Audible) animal).makeSound();// 개, 고양이
    } else if (animal instanceof SilentAnimal){
        System.out.println("소리를 내지 않는 동물");// 물고기
    }
}

[정리]

  • 추상클래스는 다중 상속이 안되지만, 인터페이스는 다중 상속이 가능하다.
  • 추상 클래스는 객체로 생성될 수 없는 클래스로 자식 클래스에서 확장될 수 있도록 만들어진 클래스이다.
  • 추상 클래스는 일반 메서드와 추상 메서드 모두 가질 수 있어서 기본적인 구현을 추상 클래스에서 하고, 하위 클래스에서는 고유의 동작을 확장하기 위해 사용한다.
  • 인터페이스는 클래스는 반드시 메소드들의 동작을 정의해야 하며, 해당 클래스는 동일한 사용 방법과 동작을 보장할 수 있다.
  • 인터페이스를 통해 다형성이 가능해진다.
728x90
반응형

'백엔드 > Java' 카테고리의 다른 글

[Java] Java Collection 한번에 정리하기  (0) 2023.04.03
oop의 특징 (캡슐화, 상속, 추상화, 다형성)  (0) 2023.03.17
Array, ArrayList  (0) 2023.03.17
Java 실행 원리와 JVM  (0) 2022.11.13
static keyword 정적 키워드  (0) 2022.11.06
728x90
반응형

개념적인 이야기보다 제가 놓치기 쉬운 것들을 기록하였습니다.

상속

  • java의 모든 클래스는 Object 클래스를 상속받는다.
  • Object 클래스의 메서드 오버라이딩
    • toString()
  • super : 상위 클래스의 값을 가져올 수 있도록 도와준다.
  • 다중 상속 : C++은 다중 상속이 되지만, 자바에서는 허용하지 않는다. (다중 상속을 하면 프로그램이 복잡해져서 그런가..?)
    • 그래서 차례대로 하나씩 상속시키면 된다. (상속 계층)
    • 하위 클래스 변수를 담을 수 있는 상위 클래스 참조 변수를 만들 수 있다! → 다형성 핵심 ( 부모는 자식을 언제나 담을 수 있다 )
  • instanceof : 현재 객체가 상위 클래스의 인스턴스인지 확인할 수 있다. 맞으면 true를 리턴해줌. (하위 클래스) instanceof (상위 클래스) → true

추상화

  • 추상 메소드를 정의하려면, 추상 클래스를 정의해야 한다.
  • 하위 클래스들이 추상 메소드의 사용방법을 적용한다. → 추상클래스를 상속해서 구상 클래스 만듬.
  • 추상클래스는 새로운 인스턴스를 만들 수 없다.
  • 추상클래스 안에는 비추상적 메소드도 가질 수 있다.

다형성

인터페이스

  • 하나에 여러 개의 적용법이 있을 수 있다. —> 다형성의 핵심 개념. 같은 것에 여러 가지 구현을 부여할 수 있다.
  • 인터페이스는 공통적인 시행 가능 행동들을 대표하는 것이다. 공통 행동을 클래스에게 전달하는 역할.
  • 특정 클래스가 확실히 구현할 메소드들이 무엇인지, 시스템 안의 모든 다른 클래스들은 그 특정 클래스가 모든 메소드들을 담을 것을 기대한다.
  • 인터페이스는 또다른 인터페이스를 상속할 수 있다.
  • 인터페이스 안에서는 변수가 아니라 상수만 선언할 수 있다.

인터페이스 vs 추상화

  • 별다른 관계는 없다. 문법이 비슷해보일 뿐이다.
  • 인터페이스는 언제 사용하는 것일까? : 두 시스템 사이에 소통하길 원하거나 소통 방식을 정하고 싶을때. → implements
  • 추상클래스는 높은 단계의 구조를 제공하고 싶어 할 때 사용. 구현의 세세한 부분들은 하위 클래스에 맡기고 싶을 때. → 상속 extends
  • 인터페이스 안에는 어떤 메소드도 private으로 선언할 수 없다. 모든 것은 public
  • 인터페이스 안에는 변수들을 넣을 수 없다. 모든 것은 상수.
  • 한 클래스는 여러 인터페이스들은 구현할 수 있지만, 여러 추상적 클래스들을 상속할 수는 없다.

다형성

  • 다형성은 인터페이스에 적용되는 만큼 상속 개념에도 적용된다.
728x90
반응형

'백엔드 > Java' 카테고리의 다른 글

[Java] Java Collection 한번에 정리하기  (0) 2023.04.03
[Java] 추상클래스 vs 인터페이스  (0) 2023.03.23
Array, ArrayList  (0) 2023.03.17
Java 실행 원리와 JVM  (0) 2022.11.13
static keyword 정적 키워드  (0) 2022.11.06

+ Recent posts