반응형

 


 

 

 

전체적인 인증 흐름

Filter(username, password 토큰 생성) ->  Manager(Provider 구분) ->  Provider(실제 인증 처리부) ->

 

Service(username으로 DB에서 조회) ->  Details(User정보 구성) ->

 

Provider(입력 token과 Details 비교 검증, 일치할 시 Authentication 리턴) -> 

 

Filter(SpringContext에 Authentication 등록)

 

 

간단 요약

(입력된 username, password 토큰)을

(username으로 DB에서 조회한 유저정보)랑 비교 검증

일치하면 (Authentication 권한과 정보)를 SecurityContext에 등록

스택을 쌓아가며 인증처리를 한다.

 


 

AuthenticationFilter

인증의 처음과 마지막을 제어

 

처음

form 값으로 UsernamePassworAuthenticationToken을 만든다.

 

마지막

Security Session(Context)에 인터페이스 Authentication를 넣는다.

구현체 UsernamePassworAuthenticationToken는두개의 생성자를 갖고 있다. (인증 전, 인증 후)

 

 


 

전체 로직 처리는 AuthenticationManager에서 한다.

 

Provider 종류에 알맞게 인증 처리하는 곳 (Factory)

실제 인증은 AuthenticationProvider에서 처리

for (AuthenticationProvider provider : getProviders()) {
   if (!provider.supports(toTest)) {
      continue;
   }
   if (logger.isTraceEnabled()) {
      logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
            provider.getClass().getSimpleName(), ++currentPosition, size));
   }
   try {
      result = provider.authenticate(authentication);
      if (result != null) {
         copyDetails(authentication, result);
         break;
      }
   }

등록된 provider를 반복문으로 돌려 해당되는 provider를 찾는다.

Token(Username, password)DB정보(UserDetail)와 검증해서 authenticate를 통과하나 확인한다.

토큰과 userDetail이 일치하는 인증된 사용자이므로 break로 반복문 종료.

 

 

if (result != null) {
   if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
      // Authentication is complete. Remove credentials and other secret data
      // from authentication
      ((CredentialsContainer) result).eraseCredentials();
   }
   // If the parent AuthenticationManager was attempted and successful then it
   // will publish an AuthenticationSuccessEvent
   // This check prevents a duplicate AuthenticationSuccessEvent if the parent
   // AuthenticationManager already published it
   if (parentResult == null) {
      this.eventPublisher.publishAuthenticationSuccess(result);
   }

   return result;
}

인증되면 Authenticaton 객체를 AuthenticationFilter에게 return

 


 

실질적인 인증 처리는 AuthenticationProvider에서 한다.

 

인증 전 Authentication 객체를 받아서 인증 후 객체로 만드는 곳

Provider는 여러개일 수 있다.

해당되는 Provider로 인증을 진행한다.

 

    UserDetailService (Login 요청이 오면 loadUserByUserName 메서드 동작), 인터페이스이므로 구현

    Username으로만 Repository를 이용해 DB에 조회한다.

    유저가 있다면 UserDetails로 반환한다.

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
   final UserRepository userRepository;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      Optional<User> optionalUser = userRepository.findByUsername(username);
      if(optionalUser.isPresent()){
         return new PrincipalDetail(optionalUser.get()); 
      }else{
         return null;
      }
   }
}

 

 

 

    UserDetails, 인터페이스이므로 구현해서 유저의 정보를 제공 (UserDetailService에서 생성된다.)

    사용자의 정보 제공 목적으로 사용된다. (DB데이터, 로그인된 유저 Session)

    유저명, 비밀번호, 유저 권한 등을 갖고 있다.

    생성자에 User를 넣어준다.

public class PrincipalDetail implements UserDetails {
   private User user;

   public PrincipalDetail(User user){
      this.user = user;
   }

   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
      Collection<GrantedAuthority> collect = new ArrayList<>();
      collect.add(new GrantedAuthority() {
         @Override
         public String getAuthority() {
            return user.getRole().name();
         }
      });

      return collect;
   }

   @Override
   public String getPassword() {
      return user.getPassword();
   }

   @Override
   public String getUsername() {
      return user.getUsername();
   }

   @Override
   public boolean isAccountNonExpired() {
      return true;
   }

   @Override
   public boolean isAccountNonLocked() {
      return true;
   }

   @Override
   public boolean isCredentialsNonExpired() {
      return true;
   }

   @Override
   public boolean isEnabled() {
      return true;
   }
}

 

 

 

UserDetails 객체는 UsernamePasswordAuthenticationToken(Authentication)을 생성, 보관한다.

 

AuthenticationProvider에서

전달받은 UserDetails와 UsernamePassworAuthenticationToken(Authentication) 을 검증하여 인증처리

암호가 같다면 인증처리된 Authentication을 AuthenticationManager에게 반환

(이전에 password를 해시암호화 해야한다.)

 

AuthenticationManager에서 AuthenticationFilter에게 반환

Authentication를 SecurityContext에 저장

 

 


 

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {

   @Bean
   public BCryptPasswordEncoder passwordEncoder(){
      return new BCryptPasswordEncoder();
   }

   @Bean // 필터를 bean component 생성
   public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  // 원하는 필터를 골라서 커스텀 적용시킬 수 있다.
      return http
              // 권한 설정
              .csrf().disable() // 해당 설정 안하면 csrf 토큰이 없어서, post 는 forbidden 이 나오므로 테스트에선 해제
              .authorizeRequests()
              .antMatchers("/user/**").hasRole("USER") // ROLE 이 필요한 URL 을 먼저 적용 시킨다.
              .antMatchers("/manager/**").hasRole("MANAGER")
              .antMatchers("/login").not().hasAnyRole("USER", "MANAGER") // 로그인 이후 접근 금지
              .antMatchers("/**").permitAll()

              // 로그인
              .and()
              .formLogin() // 해당 url 은 form 으로 이동
              .loginPage("/login")
              .loginProcessingUrl("/login") // 시큐리티가 로그인 대신 진행
              .defaultSuccessUrl("/")
              .failureUrl("/login")

              // 로그아웃
              .and()
              .logout()
              .logoutSuccessUrl("/")
              .deleteCookies("JSESSIONID")

              .and().build();

   }
}

 

 

 


 

https://mangkyu.tistory.com/76

반응형

'프로그래밍 > Security' 카테고리의 다른 글

웹 요청 정리  (0) 2022.10.20
[쿠키] Javascript로 쿠키에 직접 접근, HttpOnly, CORS  (0) 2022.10.19
[Web 보안] XSS, CSRF  (0) 2022.08.25
[인증과 인가] 쿠키와 세션, JWT  (0) 2022.06.23
반응형

 

쓰기와 쓰기가 동시에 작업하는 것이 불가능

읽기와 읽기는 동시에 언제든 가능

 

문제 부분은 쓰기 트랜잭션읽기 트랜잭션이 동시에 일어날 때

 

하나의 트랜잭션은 발생 시점부터 데이터 일관성을 가져야 한다.

 

 

 

Read uncommited - 오손 읽기(Dirty read) 

commit되지않은 데이터를 읽는 문제

 

 

Read commited - 반복 불가능 읽기(Non repeatable read)

update -> commit된 데이터를 읽어, 데이터 일관성이 깨진다.

 

 

Repeatable read - 유령 데이터 읽기(Phantom read)

트랜잭션 시작 전 commit 된 데이터만 undo영역을 이용하여 읽는다. 일관성이 깨지지 않는다.

insert -> commit 된 데이터를 읽어, 데이터를 추가로 읽어 데이터 일관성이 깨진다.

 

 

Serializable - 모든 문제 해결

트랜잭션이 다른 트랜잭션으로 부터 완전히 독립

 

 

 

 

MySQL은 Repeatable read를 사용하므로 유령 데이터 읽기가 발생할 수 있지만,

inno db 넥스트 키락으로 row에 락을걸어 insert 트랜잭션을 막기때문에, 유령 데이터 읽기가 발생하지 않는다.

 


 

https://idea-sketch.tistory.com/46

 

[MySQL]MySQL 벼락치기(5) - 갭락(Gap Lock)과 넥스트 키 락(Next-Key Lock)

이번 포스팅은 사내에서 MySQL 관련 내용 발표를 위해 Real MySQL(http://wikibook.co.kr/real-mysql/) 서적을 기반으로 학습하고 이해한 내용을 정리하는 포스팅이다. 포스팅에서는 주로 InnoDB 스토리지 엔진

idea-sketch.tistory.com

 

반응형
반응형

 


 

JPA 테스트 중에 분명 fetch LAZY로 설정했는데, 

 

sql을 보니 LAZY Entity까지 조인을 하면서 마치 EAGER처럼 동작을 하는 문제가 있었다.

 

 


 

 

@OneToMany(mappedBy = "user1", fetch = FetchType.LAZY)
private List<UserParty> userParty;

 

@Test
void test9(){
    itemRepository.findByItemNameLikeAndUser1_NameLike("%2", "c%").forEach(System.out::println);
    System.out.println("-----");
    // lazy 인데 왜 userParty 쿼리가 나갈까

}

 

itemRepository에서 user1을 사용하는 쿼리 메서드

 

User1 ---> UserParty는 분명 LAZY인데 마치 EAGER처럼 UserParty를 전부 가져왔다.

 

 

문제 상황은 [ Item ---> User1 ---> UserParty ] 까지 전이되어 전부가져옴

 

원하는 것은 [ Item ---> User1 ] 까지

 


 

해결은 단순했다.

 

@OneToMany(mappedBy = "user1", fetch = FetchType.LAZY)
@ToString.Exclude
private List<UserParty> userParty;

 

Entity 상단에 설정된 @ToString 때문에 getter가 동작하여 EAGER처럼 계속 조회해서 사용하는 것이었다.

 

@ToString.Exclude를 붙여서 getter를 호출하지 않게 설정하고 LAZY로 정상 작동되게 했다.

 

fetch LAZY 를 설정하면 ToString.Exclude 를 같이 사용

반응형
반응형

App과 DB 사이에 있는 가상의 DB라고 생각하는 것이 직관적인 것 같다.

 

EntityManager Factory가 스레드마다 DB에 접근 시 EntityManager를 생성한다. 

EntityManager가 영속성 컨텍스트를 관리한다.

 

 

https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80

 

JPA 영속성 컨텍스트란?

영속성 컨텐스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.em.persist

velog.io

 

 


목차

  • 1차 캐시
  • 더티 체킹
  • 쓰기 지연
  • 동일성 보장

 

  • 영속성 컨텍스트의 4가지 상태

 

  • transaction

 


영속성 콘텍스트의 기능

 

 

1차 캐시

Map의 형태로 Key는 idValue는 Entity

 

  1. [1차 캐시]에 조회하고자 하는 id가 있는지 확인한다.

(persist 이후 1차 캐시에 있다면 조회 쿼리가 동작하지 않고 찾아온다.)

  2. 1차 캐시에 없다면 [DB]에 있는지 확인한다. (DB select 쿼리가 동작한다.)

1차 캐시를 먼저 확인하고, DB에 접근하는 방식이다. 캐시를 재사용하므로 성능이 향상된다.

 

 

 

Dirty Checking

상태 변화 감지

영속성 콘텍스트의 최초 조회한 상태(스냅샷)가 변경되면 트랜잭션 종료 시점에서 자동으로 해당 Entity를 update 한다.

persist로 Entity의 등록 상태가 아니라 조회를 한 상태여야 한다.

조회한 Entity를 변경하면 detach Entity가 생성 되는데 detach Entity를 merge -> update한다. (자세한 내용은 아래에)

 

 

 

트랜잭션을 이용한 쓰기 지연

영속성 컨텍스트에서 발생하는 sql쿼리를 모아놨다가 트랜잰션이 종료될 때 모든 sql쿼리를 반영한다.

 

@Transactional 안에 save()가 있다면, 즉시 반영이 아니라 해당 메서드가 종료될 때 반영 (@Transactional이 상위이므로)

 

 


영속성 콘텍스트 4가지 상태

 

 

 

 

New 상태

객체를 생성한 상태, 객체를 생성했다고 해서 Managed가 아니다. (persist필요)

영속성 콘텍스트에 Entity가 등록되지 않은 상태

 

Manage 상태

영속성 콘텍스트에 Entity가 등록되어 관리되고 있는 상태

 

Remove 상태

영속성 콘텍스트에 Entity가 등록되어 관리되고 있다가 삭제된 상태

 

Detach 상태

영속성 콘텍스트에서 관리되지 않도록 분리한다. (1차 캐시, Dirty Checking이 동작되지 않는다.)

EntityManager에서만 Detach 상태를 만들 수 있고, JpaRepository를 이용해서는 불가능하다.

(JPA에서는 굳이 직접 Detach 상태로 만드는 것이 의미가 없다고 생각했는지)

 

EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();

// 기존 엔티티를 가져와서 Detached 상태로 변경
MyEntity detachedEntity = entityManager.find(MyEntity.class, entityId);
entityManager.detach(detachedEntity);

// Detached 상태의 엔티티 수정
detachedEntity.setName("Updated Name");

// 수정된 내용이 반영된 새로운 엔티티를 영속성 컨텍스트에서 관리
MyEntity managedEntity = entityManager.merge(detachedEntity);
managedEntity.setAnotherProperty("Updated Property");

transaction.commit();

기존 Entity는 detached가 되고, update한 내용인 Entity는 managed 상태가 된다.

 

 


트랜잭션 - 지연 쓰기, 캐시

 

 

✔ @Transactional

트랜잭션이 종료되면 영속성 콘텍스트를 DB에 반영하고, 종료한다. (생명주기가 같다고 본다.)

반영할 sql을 영속성 콘텍스트에 모아두었다가 한 번에 처리해주는 역할

 

 

 

✔ flush()

영속성 컨텍스트를 DB와 동기화한다. (Managed와 DB를 맞춘다.)

(영속성 컨테이너에 Entity는 소멸되지 않는다.)

1. 트랜잭션 종료

2. flush() 호출

3. jpql 쿼리 동작

 

 

 

✔ AutoFlush 

jpql 쿼리 실행시 자동으로 flush() -> jpql 동작

jqpl을 사용하면 DB에 접근하게 되는데, 영속성 컨텍스트 내용을 반영하고, 다시 가져온다.

동작에 이상없도록 하기 위한 JPA의 설정이다.

 

 

 

✔ save()

save()는 하나의 트랙잭션이다(@Transactional Propagation required가 달려있다.)

트랜잭션이므로 호출 시점에서 영속성 콘텍스트 내용을 DB에 반영하고, 종료한다.

 

하지만 상위 @Transactional로 묶여있다면 save는 DB에 저장하겠다는 의미가 아니다.

상위 트랜잭션에 묶여 사용된다. (Propagation)

 

save에서 persist & merge의 이해

persist - 새로운 객체를 영속성 콘텍스트에서 등록 관리한다. 트랜잭션 종료 시 insert

merge - detach Entity를 manage상태로 만든다. 트랜잭션 종료 시 update

 

Detach Entity는 단순히 볼게 아니다.

처음 key 1을 갖고있는 Entity는 manage상태이고

같은 키를 갖는 새로 생성한 Entity는 detach 상태가 된다. (키가 같기 때문에)

키가 같은 Entity는 detach상태이고, merge가 동작하여 update가 된다.

기존의 Entity를 조회하여 set...()으로 값을 변경했을 때도 마찬가지로 detach Entity상태가 되고, merge 동작하게 된다.

 

void 영속성상태테스트(){
    // 해당 key 가 없으면 insert
    MyUser myUser = new MyUser();
    myUser.setId(1L);
    myUser.setName("Park");
    myUserRepository.save(myUser); // insert
    System.out.println(myUserRepository.findById(1L));

    // 해당 key 가 있으면 update
    MyUser myUser1 = new MyUser();
    myUser1.setId(1L);
    myUser1.setName("Park123");
    myUserRepository.save(myUser1); 
    System.out.println(myUserRepository.findById(1L));

    // 해당 key 가 있으면 update
    MyUser myUser2 = myUserRepository.findById(1L).orElseThrow(RuntimeException::new);
    myUser2.setName("Park456");
    myUserRepository.save(myUser2); 
    System.out.println(myUserRepository.findById(1L));
}

첫번째 save는 insert

두번째, 세번째 save 는 update가 동작한다. save()에서 select로 해당 키를 가져와서 merge -> update한다.

간단하게 새로운 id라면 insert, 기존 DB에 있는 id라면 update

 

 

 

✔ 예외처리

checked - 컴파일 타임 때 알 수 있으므로 무조건 예외 처리된다는 전제하에 RollBack 되지 않고 DB에 반영된다. (실제 예외가 발생해도 commit)

unchecked - 개발자가 놓칠 수 있으므로 예외 발생 시 RollBack

 

 

 

✔ Transaction Propagation

(default) - required 상위 트랜잭션이 있다면 유지해서 사용, 없다면 생성

support - 상위 트랜잭션이 있다면 사용, 없다면 없이 진행

notSupport - 해당 영역은 무조건 트랜잭션 없이 사용

requires_new 트랜잭션을 새로 생성해서 사용

 

nested 같은 트랜잭션을 사용하지만 종속된 트랜잭션은 상위에 영향을 주지않는다. 상위로부터는 영향받는다.

 

mandatory - 상위 트랜잭션이 있어야하고, 없으면 오류발생

never - 상위 트랜잭션이 없어야하고, 있으면 오류발생

 

 

 

 

 

 

 

 

 

 

 

 

반응형
반응형

 

 

Spring boot 기준 Filter는 Spring 외부에 있지 않다.

톰캣이 내장되어 Filter도 Di가 가능.

 

 

interceptor는 dispatcher servlet과 controller 사이에서 요청과 응답을 가공, 처리하고자 할 때 사용한다.

 

 

 

interceptor와 filter

filter는 모든 요청과 공통기능에 적용할 사항

interceptor는 선별한 요청과 응답에 대해 적용할 세부 사항

 

 

interceptor 와 AOP

interceptor는 URL을 통해 적용할 메서드를 선별

AOP는 포인트컷을 통해 적용할 메서드를 선별

 

 

 


 

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Login {
    
}

메서드에서 사용할 Login 어노테이션

 

 

 

 


 

 

 

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;

            if (hm.hasMethodAnnotation(Login.class)){
                log.info("true");
                return true;
            }
        }
        log.info("false");
        return false;
    }

LoginInterceptor

인터셉터의 preHandle에서 URL 매핑  메서드에 Login어노테이션이 있는지 확인한다.

 

return true이면 controller로 진입하여 로직을 처리한다.

return false이면 controller로 진입하지 못한다.

 

 

HandlerInterceptor의 메서드

preHandler() - 컨트롤러 로직 처리 전

postHandler() - 컨트롤러 로직 처리 후

afterCompletion() - 뷰가 렌더링된 이후에 호출된다.

예외가 발생하면 postHandler()는 동작하지 않지만, afterCompletion()은 동작한다.

 

Object handler

URL 로 접근하는 매핑된 메서드 

HandlerMethod로 형변환하여 매핑 메서드 정보를 읽어낼 수 있다.

 


 

 

 


@Configuration
public class WebMvc implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor());
    }
}

작성한 LoginInterceptor 인터셉터를 WebMvcConfigurer 에 등록한다.

WebMvcConfigurer는 어플리케이션 구동시 실행된다.

 

 

@Slf4j
@Configuration
public class WebMvc implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")// 적용할 url 패턴
                .excludePathPatterns("/ic/get2")  // 배제할 url 패턴
                .order(1);
    }
}

인터셉터를 적용할 URL 패턴을 적용하고, 제외할 수 있다.

인터셉터의 적용 순서도 적용할 수 있다.

 

URL pattern

/** [ /모든 경로 ] 

ex) user/board/1

 

/*   [ /경로 하나 ] 

ex) /ic , /user, /board

 

 

 


 

@Controller
@RequestMapping("/ic")
public class Icontroller {

    @GetMapping("/get")
    @ResponseBody
    @Login
    public String test(){
        return "hello interceptor";
    }
}

Login체크를 진행할 컨트롤러 메서드에 @Login을 붙여준다.

 


 

 

 

interceptor를 이용한 로직은 목적에 따라 달라질 수 있다.

 

 

 

 

 

 

 

이전에는 Filter가 스프링 컨테이너에 등록되지 않아, Filter에서 DI를 이용하는 것이 불가능 했다.

(Spring boot 기준) Filter 구현체를 Bean으로 등록하므로, 현재는 가능하다.

 

 

@Component
@RequiredArgsConstructor
public class FirstFilter implements Filter {

   private final FilterDiTestService filterDiTestService;

   @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      System.out.println("필터 동작");
      filterDiTestService.test();
      chain.doFilter(request,response);

   }
}

 

 


 

 

 

 

 

반응형
반응형

 

도커

도커는 컨테이너때문에 쓰는 것이다.

컨테이너는 어느 환경에서건 같은 환경을 구축할 수 있다.

 

기존 가상머신 방식이 비효율적인 이유

하나의 운영체제에 여러 가상머신을 설치해서 각 가상머신마다 다른 버전, 다른 구성으로 software 구축할 수 있다.

하지만 각 가상 머신 마다 OS가 필요하므로 메모리, 드라이브 공간을 낭비한다.

 

컨테이너 방식이 효율적인 이유

하나의 운영체제에 하나의 docker engine을 설치하여 여러 컨테이너를 도커가 관리한다.

docker engine에서 동작하는 컨테이너들은 가상 머신보다 빠르고 효율적이다.

 

이미지로 만들어서 어떤 환경에서든 이미지가 같으면 같은 컨테이너가 생성된다.

공유, 재구축, 배포가 쉬워진다.

 

 

설치

window10 pro 이상이면 docker desktop (window10에서 제공하는 가상 머신 사용)

linux는 도커 엔진 설치가 간단하다.

 

 


이미지와 컨테이너

 

이미지 -> 클래스

docker hub에 이미 만들어진 이미지 사용하거나 커스텀, 그 위에 레이어를 쌓는 방식

이미지는 빌드되면 닫힌다. ReadOnly

변경사항을 적용하려면 이미지를 다시 빌드해줘야 한다.

 

 

컨테이너 -> 인스턴스

컨테이너는 하나의 머신으로 생각, shell로 안에서 작업을 진행할 수 있다. 

하나의 이미지로 여러개의 컨테이너 인스턴스를 실행할 수 있다.

컨테이너끼리는 전혀 관계가 없다.

컨테이너마다 버전을 다르게 할 수 있다.

 


 

 

도커 이미지 만들기 [Dockerfile]

 

FROM   base 이미지 지정

ARG   이미지 빌드타임 KEY=VALUE로 사용할 수 있다.

ENV  PORT 80      환경 변수 설정, 사용시에는 $PORT, 런타임시에도 사용가능

 

WORKDIR   [컨테이너에서 작업폴더가 되는 곳    COPY, RUN 등은 이곳을 기준으로 동작]

COPY   [이미지에 들어갈 로컬 파일]  [WORKDIR이 기준] 

RUN   이미지를 빌드(생성)할 때 마다 실행할 명령어

 

 

컨테이너가 된 이후

 

EXPOSE $PORT         포트 80 노출, host와 포트 포워딩 필요

CMD 컨테이너가 실행될 때 동작할 명령어 [배열로 명령어 작성, shell이 있다면 string 가능]  

 

 

 

Dockfile 한줄 한줄이 layer다.

변경이 자주되는 layer를 아래에 두면, 윗 layer는 이전 레이어로 caching 된다.

변경 사항이 일어난 layer 밑은 전부 다시 빌드 작업을 진행한다.

 

역 슬래시를 이용해서 여러줄 작성이 가능.

 

 


데이터 volumes & bind mounts

 

데이터를 도커가 관리하고, 직접 접근은 할 필요가 없는 경우

 

✔ 익명 볼륨

컨테이너가 삭제되면 같이 제거된다.

하나의 컨테이너에서 데이터 사용

명령어: -v 볼륨경로 (볼륨명을 주지않는다. 컨테이너를 생성할 때 지정)

 

✔ 네임드 볼륨

컨테이너가 삭제되도 유지된다.

여러 컨테이너에서 데이터를 공유, 생성, 유지하고 싶을 때

명령어:-v 볼륨명:볼륨경로 (볼륨명을 준다. 컨테이너를 생성할 때 지정)

 

 

 

 

 

✔ bind mounts

host에서 직접 접근해서 사용할 필요가 있는 경우. 주로 편집을 위해 사용

도커와 host가 같이 사용하는 바인딩하여 관리하는 데이터

명령어: -v 왼쪽경로디렉토리(로컬):오른쪽 경로디렉토리(컨테이너)

 

 

bind mounts주의점

도커 컨테이너 내부 폴더보다, bind mount가 우선된다. (덮어씌운다)

 

컨테이너 내부가 bind mounts된 로컬 폴더로 뒤집어 씌어지면서 이미지 빌드할 때 설정한 종속성이 없어졌다.

덮어씌워지기 전에 이때 필요한 종속성 이미지 파일들을 익명 볼륨에 넣어 유지시켜줘야 한다.

 

도커파일에 설치명령은 먼저 동작하여 익명 볼륨에 관리시키고 (충돌이 발생하면 도커가 경로가 긴 볼륨을 먼저 보고 적용한다.)

로컬파일을 덮어씌우는 방식

 

 

컨테이너에서 유지되어야 하는 것

DB 데이터

서버 log 등...

 


네트워킹

 

✔ 컨테이너 app <ㅡ> host DB 연동

호스트 database와 컨테이너 app이 통신할 수 있다.

host.docker.internal

 

✔ 수동 컨테이너 app <ㅡ> 컨테이너 DB 연동

컨테이너 app에서 DB를 연동

database 컨테이너를 띄우고

docker container inspect로 [ip:port]를 확인한다.

 

✔ 더 개선된 컨테이너 app <ㅡ> 컨테이너 DB 연동

컨테이너들을 하나에서 네트워크로 사용하면 된다. 도커에서는 이러한 네트워크를 제공한다.

docker network create [네트워크명]

컨테이너를 생성할 때 --network [네트워크명]

컨테이너 app에서 DB를 연동하는 코드에 [ip]대신 [컨테이너DB명]:port 를 입력

 

도커 네트워크가 같다면 컨테이너 명으로 접근할 수 있다.

 

내부에서 사용되는 컨테이너 DB는 컨테이너 생성 명령어에서 -p 작업을 해주지 않아도 된다. 

외부에서 요청을 받게되는 컨테이너 app은 포트번호를 노출시켜줘야 한다.

 

 


docker compose

 

자체 네트워크로 여러 컨테이너를 동시에 띄우고 --rm default로 제거한다.

자체 네트워크를 사용하므로 네트워크를 지정해주지 않아도 된다.

 

이미지를 빌드하고, 컨테이너를 실행하는 과정을 한번에 과정으로 실행

 

docker-compose up

docker-compose down

 

docker-compose run [services 등록명]

일부만 컨테이너로 실행할 경우

 

docker-compose.yml

version: '3.4'

services:
  webmvc:
    image: eshop/webmvc
    environment:
      - CatalogUrl=http://catalog-api
      - OrderingUrl=http://ordering-api
      - BasketUrl=http://basket-api
    ports:
      - "5100:80"
    depends_on:
      - catalog-api
      - ordering-api
      - basket-api

  catalog-api:
    image: eshop/catalog-api
    environment:
      - ConnectionString=Server=sqldata;Initial Catalog=CatalogData;User Id=sa;Password=[PLACEHOLDER]
    expose:
      - "80"
    ports:
      - "5101:80"
    #extra hosts can be used for standalone SQL Server or services at the dev PC
    extra_hosts:
      - "CESARDLSURFBOOK:10.0.75.1"
    depends_on:
      - sqldata

  ordering-api:
    image: eshop/ordering-api
    environment:
      - ConnectionString=Server=sqldata;Database=Services.OrderingDb;User Id=sa;Password=[PLACEHOLDER]
    ports:
      - "5102:80"
    #extra hosts can be used for standalone SQL Server or services at the dev PC
    extra_hosts:
      - "CESARDLSURFBOOK:10.0.75.1"
    depends_on:
      - sqldata

  basket-api:
    image: eshop/basket-api
    environment:
      - ConnectionString=sqldata
    ports:
      - "5103:80"
    depends_on:
      - sqldata

  sqldata:
    environment:
      - SA_PASSWORD=[PLACEHOLDER]
      - ACCEPT_EULA=Y
    ports:
      - "5434:1433"

  basketdata:
    image: redis

 

Dockerfile을 만들어서 참조해서 사용해도 되고, docker-compose에 모든 내용을 작성해도 된다.

 


Docker util container

 

로컬에 환경설정을 하기 위해 사용되는 이미지와 컨테이너

 

도커 이미지를 기반으로 컨테이너화를 할 때,

이미지마다 해당되는 명령어를 사용하여, 추가적인 환경 설정과 툴을 설치해줄 수 있다.

 

바인드 마운트를 같이 진행하면 로컬에 환경 설정이 된다.

 

 

주의점

docker run ... [명령어] 를 실행하면

Dockerfile의 [CMD 명령어]를 덮어씌운다.

이를 방지하고자 [ENTRYPOINT 명령어]를 사용하면 ENTRYPOINT 명령어 뒤에 이어서 진행한다.

 

 

 


Docker 배포 시

 

도커를 이용해 배포할 때는 바인드 마운트 사용을 권장하지 않는다.

도커 컨테이너의 기본 아이디어는 컨테이너의 캡슐화이고,

바운드 마운트는 개발할 때의 편의성으로 사용한다.

 

AWS ECS를 이용하면 컨테이너가 동일 머신에서 동작할 것이 보장되지 않기 때문에,

도커 네트워크를 사용할 수 없다.

환경 변수 설정으로 ip를 지정하게 하자.

 

 


명령어

 

docker ps -a 모두 보기

 

 

이미지 명령어

docker build

-t repository명칭:tag명칭

-f 경로 

도커파일위치 보통 .

 

 

 

컨테이너 명령어

docker run   새로운 컨테이너 실행  

docker run -i   stdin 열린 상태로 사용

docker run -t   터미널을 생성

 

docker start    컨테이너를 재실행  

docker stop     컨테이너 중지

docker rm        컨테이너를 완전히 제거

 

--name      컨테이너 이름 지정

docker run -p 80:80 이미지명      포트연결

 

 

 

attach & detach

실행중인 컨테이너는 attach, detach 할 수 있다.

attach 모드면 실행중인 container log를 계속 수신

 

detach모드:   -d 추가

attach모드:   -a 추가

 

다시연결:   docker attach 컨테이너명

attach 컨테이너 종료시키지 않고 나오기:   ctrl + p + q

로그보기: docker [-f] logs 컨테이너명

 

 

 

interactive terminal

간단한 프로그램 실행

-it

 

shell로 접근

docker exec -it 컨테이너 bin/bash  

 

 

 

 

 

 

 

 

반응형

' > docker' 카테고리의 다른 글

Docker 다시보기 정리  (1) 2023.10.18
[Docker] 도커로 스프링 프로젝트 배포하기  (0) 2022.10.18
반응형

 

 

resoruces에 위치한 data.json을 읽어야 한다.

 

 

 

{
  "hello": "123"
}

data.json의 내용은 다음과 같다.

 

 

 

@SpringBootApplication
public class RestApIsApplication {

    public static void main(String[] args) throws IOException {
        SpringApplication.run(RestApIsApplication.class, args);

        ClassPathResource resource = new ClassPathResource("data.json");
        BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()));

        // br.readLine() 이 null 인지 검사할 때 한번 사용되므로 String 에 먼저 저장해둬야한다.
        String s = "";
        while((s = br.readLine()) != null){
            System.out.println(s);

        }
    }
}

스프링에서 resources 파일을 위해 사용되는 ClassPathResource를 사용하면 파일 경로를 src/main/resoruces/부터 바로 읽을 수 있다.

 

 

 

 

File file = new File("src/main/resources/data.json");

ClassPathResource를 안쓰고 경로를 전부 적어서 읽어도 되지만, 경로가 달라질 가능성이 있으므로 ClassPathResource 권장

 

 

 

 

사용법

ClassPathResoruce의 getInputStream() 메서드를 사용하여 InputStream으로 변환한다.

InputStream를 문자를 위한 Reader로 바꾸는 InputStreamReader로 한번 더 감싼다.

InputStreamReader를 성능 향상을 위한 보조스트림 BufferedReader로 한번 더 감싼다.

 

BufferedReader는 Line 단위로 읽는다.

 

출력을 할 때는 while(br.readLine() != null)을 하게 되면, 버퍼의 내용을 한번 소모하므로 String에 저장 대입 후 null인지 검사해야 한다.

while( (s = br.readLine())!= null)

 

 

 


String s = "";
StringBuilder sb = new StringBuilder();
while((s = br.readLine()) != null){
    sb.append(s);
}
ObjectMapper om = new ObjectMapper();
JsonNode jsonNode = om.readTree(sb.toString());
System.out.println(jsonNode);

 Json으로 사용할 경우

 

 

 

@SpringBootApplication
public class RestApIsApplication {

    public static void main(String[] args) throws IOException {
        SpringApplication.run(RestApIsApplication.class, args);

        ClassPathResource resource = new ClassPathResource("data.json");

        File file = new File("src/main/resources/data.json");
        File copyFile = new File("src/main/resources/data1.json");

        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(copyFile)));

        int size = 0;
        char[] arr = new char[1024];
        while((size = br.read(arr)) != -1){
            bw.write(arr, 0, size);
        }
        
        br.close();
        bw.flush();
        bw.close();
    }
}

파일을 복사할 경우

 

 

 

 

@SpringBootApplication
public class RestApIsApplication {

    public static void main(String[] args) throws IOException {
        SpringApplication.run(RestApIsApplication.class, args);

        ClassPathResource resource = new ClassPathResource("data.json");
        ClassPathResource resourceCopy = new ClassPathResource("data1.json");
        File file = new File(resourceCopy.getURI());

        BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));

        int size = 0;
        char[] arr = new char[1024];
        while((size = br.read(arr)) != -1){
            bw.write(arr, 0, size);
        }
        br.close();
        bw.flush();
        bw.close();
    }
}

ClassPathResource로 사용하면 build/resources/main/ 경로에 생성된다.

 

 

 

반응형
반응형

 

 

Rest API 서버에 요청을 해서 해당 값을 Controller에서 사용할 때

 

RestTemplate를 이용해서 요청을 보낼 수 있다.

* 현재는 Spring이 RestTemplate보다는 WebClient를 지원

 

@GetMapping("/test3")
public void test3() {
    // 요청할 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:8080")
            .path("/api/test")
            .build()
            .toUri();

    RestTemplate rt = new RestTemplate();
    // HTTP GET, 응답을 객체로 받기.
    String s1 = rt.getForObject(uri, String.class);

    // HTTP GET, 응답을 ResponseEntity 로 받기.
    ResponseEntity<String> responseEntity = rt.getForEntity(uri, String.class);
    System.out.println(responseEntity.getStatusCode());
    System.out.println(responseEntity.getBody());

    // HTTP GET, 응답을 JSON 을 받을 때
    // dto 객체로 받는다.
    ResponseEntity<User> responseEntity1 = rt.getForEntity(uri, User.class);


}

 

URL를 만든 후 RestTemplate를 이용하여 요청을 한다.

 

 

 

 

URI uri = UriComponentsBuilder
        .fromUriString("http://localhost:8080")
        .path("/api/test")
        .queryParam("name", "cha")
        .queryParam("age", 25)
        .build()
        .toUri();

UriComponentsBuilder의 queryParam() 메서드를 이용해 Parameter 값을 요청해줄 수 있다.

 

 

 

 

URI uri = UriComponentsBuilder
        .fromUriString("http://localhost:8080")
        .path("/api/test/{name}/{age}")
        .encode()
        .build()
        .expand("cha", 30)
        .toUri();

UriComponentsBuilder의 expand() 메서드를 이용해 PathVariable 값을 요청해줄 수 있다.

 

 

 

 

 

RestTemplate rt = new RestTemplate();
// HTTP POST, 서버에 객체를 보내서 생성하기
User user = new User("cha",33);
// uri, request 객체, response type
ResponseEntity<User> userResponseEntity = rt.postForEntity(uri, user, User.class);

POST 요청 

서버의 명세를 보고 맞게 dto 를 작성하면 된다.

 

 

 

 


RequestEntity<User> request = RequestEntity
        .post(uri)
        .contentType(MediaType.APPLICATION_JSON)
        .header("headerDummy", "asdf")
        .body(new User("user",22));

RestTemplate rt1 = new RestTemplate();
ResponseEntity<User> responseEntity = rt1.exchange(request, User.class);

HTTP header를 커스텀해서 요청을 보낼 수도 있다.

헤더를 추가할 경우 RestTemplate의 exchange() 메서드를 사용

 

 

 


kakao Oauth 요청

 

@GetMapping("/complete")
public String complete(@RequestParam String code){
    System.out.println(code);

    // uri
    URI uri = UriComponentsBuilder
            .fromUriString("https://kauth.kakao.com/oauth/token")
            .encode(StandardCharsets.UTF_8)
            .build()
            .toUri();

    // header
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add("Content-type","application/x-www-form-urlencoded;charset=utf-8");

    // body
    MultiValueMap<String, String> map = new LinkedMultiValueMap();
    map.add("grant_type" ,"authorization_code" );
    map.add("client_id","================" );
    map.add("redirect_uri","http://localhost:8080/complete" );
    map.add("code", code );

    // Header + body
    HttpEntity<Map<String, String>> httpEntity = new HttpEntity(map, httpHeaders);

    RestTemplate rt = new RestTemplate();
    ResponseEntity<HashMap> responseEntity = rt.exchange(uri, HttpMethod.POST, httpEntity, HashMap.class);
    responseEntity.getHeaders().forEach((k, v) -> System.out.println(k + " " + v));
    responseEntity.getBody().forEach((k, v) -> System.out.println(k + " " + v));

    return "completion";

 

 

 

 

 

네이버 api 사용

 

URI uri = UriComponentsBuilder
        .fromUriString("https://openapi.naver.com/v1/search/book.json")
        .queryParam("query", word)
        .queryParam("display", 20)
        .queryParam("sort", "sim")
        .encode(StandardCharsets.UTF_8)
        .build()
        .toUri();

RequestEntity requestEntity = RequestEntity
        .post(uri)
        .header("X-Naver-Client-Id", "아이디")
        .header("X-Naver-Client-Secret", " 키 ")
        .body(" ");

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<NaverBook> responseEntity = restTemplate.exchange(requestEntity, NaverBook.class);

RequestEntity

 

HttpEntity, HttpHeaders를 사용하지 않고

header, body, contentType 을 좀 더 편하게 만들어줄 수 있다.

 

 


 

정리

 

UriComponentsBuilder: 요청할 uri를 만든다.

RequestEntity: 요청[HTTP header, body]을 만들고 RestTemplate으로 요청한다.
ResponseEntity: 응답[HTTP header, body]을 RestTemplate으로부터 받아 사용한다.
RestTemplate: HTTP 프로토콜을 이용해서 서버에 요청, 전달받은 응답을 사용할 수 있게 해준다.

 

 

반응형
반응형

 

[1] goormide 에 가입하여 Blank를 선택하여 container를 만든다.

Blank를 선택하면 기본 Ubuntu만 제공된다.

 

[2] apache2 웹 서버를 설치한다.

sudo apt-get install apache2 

 

[3] apache2 웹 서버 start 

sudo service apache2 start

 

[4-1] 컨테이너 설정에서 URL과 PORT를 지정하여 접근할 수 있다.

해당 도메인 네임으로 접근할 수 있다.

 

[4-2] 포트 포워딩를 설정하여 외부에서 접근할 수 있다.

NAT: 클라우드 서버는 하나의 공인 ip에 여러 개의 사설 ip를 두고 사용자에게 하나의 사설 ip를 제공한다. 

ifconfig 으로 사설 ip 주소 확인

사설 ip는 외부에서 접근할 수 없으므로 포트 포워딩을 이용해 [공인 ip:port]로 [사설 ip:port]에 매핑시켜줘야 한다.

 

웹 서버를 로컬에 둘 경우 공유기의 설정을 변경하여 포트 포워딩하면 되지만,

goormide에서는 컨테이너 설정에서 포트 포워딩 설정이 가능

 


 

 

 

 

 

 

반응형

' > Linux' 카테고리의 다른 글

[리눅스] 쉘 스크립트  (0) 2022.10.18
리눅스 기초  (0) 2022.08.08
반응형

 

하나의 운영체제를 여러명의 유저가 사용

각각 권한이 있고 super user가 있다.

 

 

 

 

 

Kernel - 하드웨어의 자원을 관리, process, memory, IO system...

Shell - Kernel을 실행시키는 명령어 소프트웨어 [사용자와 커널의 인터페이스] 여러 종류가 있다.

 


리눅스의 특징

 

 

리눅스 명령어는 전부 하나의 process 프로그램이다.


** [명령어] [-간략]  [--풀네임] [내용]

옵션은 process에 인자를 주는 것이다.

 

 

 

 

IO redirection    [결과를 다른곳으로 out]


ls -l > hi.txt    화면결과를 hi.txt에 복사시켜놓음 [결과를 redirection]
1>     standard output 를 redirection
2>     standard error 를 redirection
<   input

>>  기존 내용에 추가로 append
<<문자  문자를 전부입력을 받다가 이 특정 문자가 나오면 입력이 끝난다.

 



package manager  

apt 도구를 이용해서 linux package repository에서 가져와 사용한다.

일종의 앱스토어 역할과 같다.

 

 

 

파이프 라인

명령어의 출력을 다른 명령어의 입력으로 사용해서 최종 출력을 내는 것

ls - al | grep 찾을단어

 

 

 

Job Controll

ctrl + Z      background에 올려둔다. 

jobs    background 실행중인 job 목록을 본다.

fg %N     foreground로 전환

bg %N    background의 stopped를 running으로 만든다.

kill %N   background 종료

 

 

 

daemon

항상 실행되어야 하는 프로그램

etc/init.d/ 에 위치한다.

sudo service 데몬 start

sudo service 데몬 stop

 

service --status-all

 


디렉토리

 

/home - 각 사용자를 위한 디렉토리

/bin - 실행 프로그램

/sbin - 시스템 프로그렘

/etc - 설정 파일

/var - 자주 변경되는 프로그램, 로그 파일

/tmp - 임시 파일, 자동 삭제

/opt - 옵션

 

/lib, /boot, /mnt, media 시스템 디렉토리들

 

 

웹서버 설정파일들은 설정파일이 있는 곳인 /etc에 있다.

설정파일을 보면 DocumentRoot가 요청에 대한 파일을 찾는 경로

로그는 /var에 담겨진다.

 

 


 

멀티 유저

 

sudo - super user do

superuser #

일반유저 $

최초 실행시 sudo passwd 유저 로 root의 비밀번호 설정, 일반 사용자 비밀번호 설정도 같은 명령어.

 

sudo useradd -m 유저명  /  유저 추가

su - 유저명  /  유저 변경

sudo usermod -a -G sudo 유저명 / 유저에게 sudo 권한 주기

 

chmod

type | user rwx,  group rwx, other rwx | 링크 수 | user, group | 파일사이즈 | 시간 | 파일명

chmod o-r 파일명   other에 read 권한이 없도록 한다.

chmod u+w 파일명   user에 write 권한을 추가한다.

chmod 771 파일명   rwx 8진수로 가능

 

그룹

groupadd 그룹명

/etc/group  에 추가된다.

usermod -a -G 그룹명 유저명   

유저 그룹에 추가

sudo chown [owner:group] .  

디렉토리 그룹 변경

 


명령어 모음

 

세미콜론 

여러 명령어를 이어서 실행

 

ls -al 현재 디렉토리의 목록들을 보여준다. [숨김파일, 형식]

cd 디렉토리 이동

mkdir 디렉토리 만들기

pwd 현재 위치 보기

rm 삭제

cat / head / tail 파일 보기

 

mv [현재경로] [이동할경로]   // 이름 변경도 가능
cp [현재경로] [이동할경로]

ps 실행중인 프로세스를 본다.

grep 문자열 검색

find / -name 파일명 파일 검색

 

who 접속 사용자 정보

whoami 내 정보

wget url 파일 다운로드

curl url 요청

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형

' > Linux' 카테고리의 다른 글

[리눅스] 쉘 스크립트  (0) 2022.10.18
[goormide] goormide 웹 서버 설치  (0) 2022.08.09

+ Recent posts