이전 글에서 Spring Security 개념에 대해서 정리했다면 이번 글은 Spring Security + JWT 를 적용해보고자 합니다. 앞서 말했다시피 학습과 병행하며 진행된 만큼 아직 개선한 여지가 있는 부분이 있습니다. 글을 읽으시다가 잘못 작성되거나 개선할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다.🙌🏻

 

 
 

목차

1. 구현해보기

  1.1 의존성 추가

  1.2 Spring Security Config

  1.3 JwtAuthorizationFilter

  1.4 CustomAuthenticationProvider

  1.5 CustomUserDetailsService

  1.6 CustomAuthSuccessHandler

  1.7 CustomAuthFailureHandler

 

2. 테스트

 

 구현해보기

 

  • Spring Boot 3.4.0
  • JDK 17
  • Build : Gradle

 

의존성 추가

dependencies {

	implementation 'org.springframework.boot:spring-boot-starter-security'
	testImplementation 'org.springframework.security:spring-security-test'

	//jwt
	implementation 'io.jsonwebtoken:jjwt:0.9.1'
	implementation 'com.sun.xml.bind:jaxb-impl:4.0.1'
	implementation 'com.sun.xml.bind:jaxb-core:4.0.1'
	implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'

}

 

 

Spring Security  Config

 

 1. SecurityFilterChain

Spring Security에서 제공하는 인증, 인가를 위한 필터들의 모임으로 사용하는 목적에 맞게 커스텀하여 필터 체인으로 포함시켜 사용할 수 있다.

 

1.1 SecurityContextPersistenceFilter

   @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
                .csrf(csrf -> csrf.disable()) // JWT 토큰을 사용으로 비활성화
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
                .addFilterBefore(jwtAuthorizationFilter(), BasicAuthenticationFilter.class) //JWT Filter

                //JWT 이용하여 인증
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                // form 기반의 로그인을 비활성화
                .formLogin(login -> login.disable())

                // cumstom으로 구성한 필터 사용
                .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class )

                .httpBasic(withDefaults());

        return http.build();
    }

 

 

 

JwtAuthorizationFilter

 

API 요청이 들어왔을 때, JWT토큰 검증을 위해 Spring Security 필터 체인에 Custom Filter를 추가했습니다.

JwtAuthorizationFilter 는 토큰이 필요하지 않는 API를 제외하고는 모두 수행되며, Authorization 헤더를 통해 JWT의 유효성 검사를 진행합니다. 

@Component
@Slf4j
public class JwtAuthorizationFilter extends OncePerRequestFilter {


    private String Bearer = "Bearer ";
    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 토큰이 필요하지 않는 API의 경우 다음 필터로 이동
        if(request.getRequestURI().startsWith("/api/auth")) {
            response.addHeader("X-Custom-Filter", "Public Access");
            filterChain.doFilter(request, response);

            return;
        }

        try {

            // Client에서 API를 요청할 때 Header 확인
            String token = request.getHeader(HttpHeaders.AUTHORIZATION);

            if(token == null || "".equals(token)) {
                log.error("Token is null");
                setErrorResponse(response, ErrorCode.NOT_EXISTS_TOKEN,null);
            }

            token = token.substring(Bearer.length());

            if(!jwtTokenUtil.isTokenValid(token)) {
                setErrorResponse(response, ErrorCode.INVALID_TOKEN, null);
            }

            filterChain.doFilter(request,response);

        } catch (Exception e) {
            setErrorResponse(response,null, e);
        }

    }

    public static void setErrorResponse(HttpServletResponse response, ErrorCode errorCode, Exception e) throws IOException {

        response.setContentType("application/json;charset=UTF-8");

        if(!"null".equals(e)) {

            if(e instanceof ExpiredJwtException) {
                errorCode = ErrorCode.EXPIRED_TOKEN;
            } else if (e instanceof SignatureException | e instanceof JwtException) {
                errorCode = ErrorCode.INVALID_TOKEN;
            } else if (e instanceof NestedServletException || e instanceof Exception) {
                errorCode = ErrorCode.INTERNAL_SERVER_ERROR;
            }
        }

        PrintWriter writer = response.getWriter();

        response.setStatus(errorCode.getStatus().value());

        HashMap<String, String> jsonMap = new HashMap<>();
        jsonMap.put("status", String.valueOf(errorCode.getStatus().value()));
        jsonMap.put("message", errorCode.getMessage());

        writer.print(JSONObject.toJSONString(jsonMap));
        writer.flush();
        writer.close();

    }
}

 

 

CustomAuthenticationFilter

 

로그인 API인 /api/auth/login 으로 요청이 들어오면 해당 필터에 들어오게 되는데, 인증 요청을 처리하는 핵심 인터페이스로 실제 인증 로직을 실행하는 부분입니다.

 

@Slf4j
@Component
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

    // login 요청을 하면 실행되는 함수
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

        log.info("1. CustomAuthenticationFilter");
        UsernamePasswordAuthenticationToken authenticationToken = null;
        try {

            // request 파라미터로 임시 토큰 발급
            ObjectMapper objectMapper = new ObjectMapper();
            User user = objectMapper.readValue(request.getInputStream(), User.class);

            authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserId(), user.getPassword());

            log.info("Principal : {}", authenticationToken.getPrincipal());
            log.info("Credentials : {}", authenticationToken.getCredentials());

            setDetails(request,authenticationToken);


        } catch (Exception e) {
            log.error(e.getMessage());
        }

        return this.getAuthenticationManager().authenticate(authenticationToken);
    }

}

 

 

 

CustomAuthenticationProvider

 

전달받은 사용자의 아이디와 비밀번호를 기반으로 비즈니스 로직을 처리하여 사용자의 '인증'에 대해서 검증을 수행하는 클래스 입니다.

CustomAuthenticationFilter로부터 생성한 토큰을 통해 'CustomUserDetailsService'를 통해 데이터베이스 내에서 사용자 정보를 조회합니다.

 

@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Resource
    private CustomUserDetailsService customUserDetailsService;
    @NonNull
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        log.info("2. CustomAuthenticationProvider");

        String userId = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        //사용자 인증에 대해서 검증 수행

        UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId);

        // DB정보랑 일치한 지 확인
        if(!passwordEncoder.matches(password, userDetails.getPassword())){
            throw new BadCredentialsException("User Info Mismatch");
        }

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());

        return authenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

 

CustomUserDetailsService

 

사용자 정보를 데이터베이스에서 가져오는 부분입니다.

@Service
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {

        log.info("UserDetailsService {}" , userId);

        User user = userRepository.findByUserId(userId)
                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + userId));
        
        return new PrincipalDetails(user);
    }
}

 

CustomAuthSuccessHandler

 

사용자의 '인증'에 대해 성공했을 경우 수행되는 Handler로 성공에 대한 사용자에게 반환값을 구성하여 전달하는 클래스입니다.

@Slf4j
@Configuration
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private JwtProperties jwtProperties;

    @Autowired
    private UserService userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("3.1 CustomAuthSuccessHandler");

        String userId = (String) authentication.getPrincipal();

        // JWT토큰 발급
        String accessToken = jwtTokenUtil.generateAccessToken(userId);
        String refreshToken = jwtTokenUtil.generateRefreshToken(userId);

        // 사용자 refreshToken DB에 저장
        userService.updateUserRefreshToken(userId, refreshToken);

        // 응답규격 생성
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);
        response.addHeader(jwtProperties.getHeaderString(), jwtProperties.getTokenPrefix()+accessToken);

        HashMap<String, String> jsonMap = new HashMap<>();
        jsonMap.put("status", String.valueOf(HttpServletResponse.SC_OK));
        jsonMap.put("message", "정상 처리되었습니다.");

        PrintWriter writer = response.getWriter();
        writer.print(JSONObject.toJSONString(jsonMap));
        writer.flush();
        writer.close();

    }
}

 

CustomAuthFailureHandler

 

사용자의 '인증'에 대해 실패했을 경우 수행되는 Handler로 실패에 대한 사용자에게 반환값을 구성하여 전달하는 클래스입니다.

@Slf4j
@Configuration
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("3.2 CustomAuthFailureHandler");

        response.setContentType("application/json;charset=UTF-8");

        ErrorCode errorCode = null;

        if(exception instanceof UsernameNotFoundException || exception instanceof BadCredentialsException){
            errorCode = ErrorCode.RESOURCE_NOT_FOUND;
        }  else if (exception instanceof InternalAuthenticationServiceException || exception instanceof AuthenticationCredentialsNotFoundException) {
            errorCode = ErrorCode.UNAUTHORIZED;
        } else {
            errorCode = ErrorCode.INTERNAL_SERVER_ERROR;
        }

        PrintWriter writer = response.getWriter();

        response.setStatus(errorCode.getStatus().value());

        HashMap<String, String> jsonMap = new HashMap<>();
        jsonMap.put("status", String.valueOf(errorCode.getStatus().value()));
        jsonMap.put("message", errorCode.getMessage());

        writer.print(JSONObject.toJSONString(jsonMap));
        writer.flush();
        writer.close();

    }
}

 

 

 테스트

 

1. 회원가입

회원가입 API
DB에 해당 사용자정보 저장

 

2. 로그인 

 

case1) 일치한 사용자 정보 없음

 

 

case2) 정상로그인 - Headers에 AccessToken 값 확인가능

'정리 > Spring' 카테고리의 다른 글

Spring Security 란?  (0) 2024.12.10
[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09

 

해당 글은 올해 팀 내 PoC로 진행했던 E2E 테스트 자동화 솔루션 개발 프로젝트를 진행했던 내용을 기록하고자 합니다.

프로젝트 초기 구축을 맡아 진행하면서 인증과 권한 권리를 위해 Spring Security와 JWT를 도입했으며, 회원가입과 로그인 같은 기본적인 인증 절차를 구축하며 프레임워크를 학습하고 적용하는 과정을 통해 많은 것을 배울 수 있었습니다.

다만, 이번 프로젝트는 학습과 병행하며 진행된 만큼 아직 개선한 여지가 있는 부분이 있습니다. 글을 읽으시다가 잘못 작성되거나 개선할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다.🙌🏻

 

 

 
 

목차

1. Spring Security 란?

2. Spring Security 사용하면 뭐가 좋을까?

3. Spring Security 동작 구조를 알아보자.

4. 더 자세히 알아보자.

 

 

 

 Spring Security 란?

Java 애플리케이션 인증(Authentication)과 권한 부여(Authorization)를 모두 제공하는 데 중점을 둔 보안 프레임워크로, Spring Framework를 기반으로 한 애플리케이션에 사용되며, 다양한 보안 관련 요구 사항을 쉽게 해결할 수 있도록 설계되었습니다.

 

주요 특징으로는 

1. 인증(Authentication)

  • 사용자가 누구인지 확인하는 과정으로, 예를 들어 사용자가 로그인 폼에서 입력한 아이디와 비밀번호를 통해 본인 여부를 확인합니다.

2. 권한 부여(Authorization)

  • 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다. 부여받은 권한에 따라 접근할 수 있는 페이지가 다를 수 있는데 예를 들어, 관리자는 모든 페이지에 접근할 수 있지만 일반 사용자는 특정 페이지에만 접근할 수 있도록 설정할 수 있습니다.

3. 유연성

  • 데이터베이스 기반 인증, OAuth2, SMAL, LDAP 등의 다양한 인증 방식을 지원하며, 사용자 정의 인증 및 권한 부여 로직을 쉽게 구현할 수 있습니다.

4. 보안 기능 내장

  • 세션 고정, CSRF(Cross-Site Request Forgery) 등과 같은 공격으로부터 보호받을 수 있습니다.

Spring Security 사용하면 뭐가 좋을까?

애플리케이션 보안 기능을 효과적으로 추가하고 관리할 수 있습니다.

 

1. 로그인과 인증 기능을 간편하게 구현

예를 들어, 한 명의 사용자가 쇼핑몰 웹 사이트에 아이디와 비밀번호로 로그인한다고 가정해보면

Spring Security는 로그인 요청을 처리하고 사용자 정보를 데이터베이스에서 확인한 뒤, 인증이 성공하면 자동으로 세션을 생성하고 인증상태를 유지하게 됩니다. 이 과정에서 별도의 인증 로직을 처음부터 구현할 필요 없이, Spring Security에서 제공하는 기능을 활용할 수 있습니다.

 

즉, 인증 로직에 대한 개발 시간 절약과 표준화된 방식으로 높은 안정성을 제공할 수 있습니다.

 

2. URL 별 접근 권한 제어

권한 별 접근할 수 있는 페이지를 세부적으로 설정할 수 있습니다.

예를 들어 쇼핑몰 관리자 페이지 (/admin)은 관리자 권한을 가진 사용자만 접근할 수 있어야하며, 그 외 일반 사용자는 상품 정보, 자신의 주문 내역 등 볼 수 있는 페이지를 제한해야 합니다.

 

Spring Security를 사용하면 아래와 같이 간단한 설정으로 권한 관리를 할 수 있습니다.

http
    .authorizeRequests()
        .antMatchers("/admin").hasRole("ADMIN")  // 관리자만 접근 가능
        .antMatchers("/user/**").hasRole("USER")  // 일반 사용자 접근 가능
        .anyRequest().authenticated();  // 나머지 요청은 인증만 되면 접근 가능

 

3. 다양한 인증 방식 지원

OAuth 2.0 및 JWT와 같은 인증 방식을 통합하여 사용자 인증을 쉽게 처리할 수 있습니다.

REST API를 개발하면서 JWT을 사용해 인증 토큰을 발급하고, 이를 통해 클라이언트와 서버 간 인증을 처리할 수 있습니다.

 

이처럼 Spring Security는 개발자에게 반복적인 보완 관련 작업을 간소화하고, 표준화된 방식으로 높은 보안 수준을 제공하기 때문에 개발효율을 높일 수 있습니다.

 


Spring Security 동작 구조에  대해서 알아보자.

Spring Security는 요청(Request)이 애플리케이션에 도달하기 전에 보안 필터(Security Filters)를 통해 인증과 권한 부여를 처리하는 구조로 설계되어 있습니다. 동작 구조를 주요 단계별로 보면

 

1단계. 요청(Request)이 Security Filter Chain에 진입

Spring Security는 HTTP 요청이 들어오면 이를 처리하기 위해 FilterChainProxy라는 메인 필터를 통해 요청을 가로챕니다.

필터 체인에서 인증 및 권한 부여를 수행하고, 검증이 완료된 이후에 다음 필터 또는 컨트롤러로 전달됩니다.

 

2단계. 인증(Authentication) 처리

사용자가 애플리케이션에 접근하려는 경우, Spring Security는 사용자 인증하는 과정을 거칩니다.

 

2.1 로그인 요청 처리

사용자가 /login과 같은 엔트포인트로 로그인 요청을 보낼 경우, UsernamePasswordAuthenticationFilter가 이를 처리합니다. 이 필터는 아이디와 비밀번호를 읽고, AuthenticationManager에게 인증을 위임합니다.

 

2.2 AuthenticationManager 동작

사용자가 입력한 정보를 기반으로 실제 인증을 수행합니다. 일반적으로 UserDetailsService를 호출하여 사용자 정보를 데이터베이스에서 서 가져와 저장된 정보와 비교하여 인증 성공 여부를 결정합니다.

 

2.3 인증 성공/실패

인증에 성공할 경우, 사용자의 인증 정보를 SecurityContext에 저장하고 다음 필터로 요청을 전달합니다.

인증에 실패할 경우, 에러 응답(401 Unauthorized 등)을 반환합니다.

 

3. 권한 부여 (Authorization) 처리

인증이 성공한 후, 사용자가 요청한 리소스에 접근할 권한이 있는지 확인하는 단계로

 

3.1 SecurityContext에서 인증 정보 확인

SecurityContext에 저장된 인증 정보를 확인하여 사용자의 역할(Role) 또는 권한(Authority)를 가져옵니다.

 

3.2 AccessDecisionManager 동작

사용자가 요청한 URL, 메서드(GET, POST등), 리소스 등에 대해 접근 가능한지 확인합니다. 권한이 없으면 403 Forbidden 에러를 반환합니다.

 

4. FilterChain 완료 및 컨트롤러 호출

모든 필터를 성공적으로 통과한 요청은 최종적으로 애플리케이션에 컨트롤러로 전달되며, 컨트롤러는 요청을 처리하고 응답을 반환합니다.

 

5. 응답 (Response) 반환 및 세션/토큰 관리

응답을 반환할 때, Spring Security는 인증 정보를 관리하거나 상태를 업데이트 합니다.

 

세션 기반 인증의 경우, 인증 정보를 세션에 저장하고 클라이언트는 세션 쿠키를 통해 인증 상태를 유지합니다.

JWT 기반 인증의 경우, 인증 성고 시 JWT 토큰을 생성하여 클라이언트에 반환합니다. 클라이언트는 이후 요청마다 이 토큰을 헤더에 포함시켜 인증을 유지합니다.

 

 

더 자세히 알아보자.

 

먼저 사용자 인증 요청이 오면

 

1. DelegatingFilterProxy 

Servlet Container 와 Spring loC Container를 연결해주는 필터로 springSecurityFilterChain 이름으로 생성된 Bean을 찾아 요청을 위임하는 역할을 한다.

 

2. FilterChainProxy

springSecurityFilterChain의 이름으로 생성되는 필터 빈으로 DelegatingFilterProxy로부터 요청을 위임 받고 실제로 보안을 처리하는 역할을 한다. (Spring Security 초기화 시 생성되는 필터들을 관리하고 제어)

 

2-1 SecurityContextPersistenceFilter

SecurityContextRepository에서 SecurityContext를 가져오거나 생성하는 역할

 

SecurityContext는 인증 객체(Authentication)가 저장되는 저장소이며 setAuthentication() 메소드를 통해 Authentication을 설정할 수 있다.

 

SecurityContextPersistenceFilter 를 거치면 SecurityContextRepository에서 SecurityContext를 가져오는데

 

1) 처음 인증하는 사용자일 경우

SecurityContext를 생성하고 SecurityContextHolder안에 저장하고 다음 필터 실행

 

2) 인증 이력이 있는 사용자일 경우

이미 존재하는 SecurityContext를 가져와 SecurityContextHolder 에 저장하고 다음 필터 실행

 

 

2-2 LogoutFilter

로그아웃에 대한 처리 담당. 세션 무효화, 인증 토큰 삭제, SecurityContext에서 토큰 삭제 등 로그아웃 시 필요한 작업 수행

 

 

2-3 UsernamePasswordAuthenticationFilter

Form Based Authentication 방식으로 인증을 처리할 때 아이디, 패스워드 데이터를 파싱하여 인증 요청을 위임하는 필터

해당 인증 방식외에 다른 로그인 방식을 사용한다면 disable() 을 통해 폼 기반 로그인 방식 비활성화 할 수 있다.

 

HttpServletRequest 객체에서 아이디, 패스워드 정보를 가져와 Authentication  객체에 저장하고 AuthenticationManager에게 인증처리를 전달한다.

 

2-4 ConCurrentSessionFilter

현재 사용자 계정으로 인증을 받은 사용자가 두 명 이상힐 때 실행되는 필터로 매 요청마다 사용자의 세션 만료 여부를 체크하고 세션이 만료되었을 경우 즉시 만료처리 하는 역할

 

2-5 RememberMeAuthenticationFilter

세션이 만료되거나 무효화되어서 세션안에 있는 SecurityContext 내 인증 객체가 null일 경우 실행되는 필터로 사용자가 요청하는 Reqeust Header에 remeber-me cookie 값을 헤더에 저장한 상태로 요청이 왔을 때 접속한 사용자 대신 인증처리를 시도

 

2-6 AnonymousAuthenticationFilter

인증 시도를 하지 않고 권한도 없이 어떤 자원에 바로 접속을 시도하는 경우 실행되는 필터로 annonymouseAuthenticationToken을 만들어 SecurityContext 객체에 저장하는 역할

 

2-7 SessionManagementFilter

현재 세션에 SecurityContext가 없거나 세션이 null인 경우에 동작되는 필터로 아래 3가지 작업 수행

 

Register SessionInfo : 사용자 세션 정보 등록

인증에 성공한 SecurityContext를 세션에 저장

 

SessionFixation : 인증을 시도하기 전 이전 쿠키가 삭제되고 성공한 시점에 새로운 쿠키 발급

 

ConcurrentSession : 사용자 계정으로 동일한 세션이 존재하는지 확인

 

2-8 ExceptionTranslationFilter

Filter Chain을 거치면서  발생하는 인증, 인가 예외가 발생할 때 실행되는 필터

 

AuthenticationException : 인증 예외 처리

 

AccessDeniedException : 인가 예외 처리

 

2-9 FilterSecurityInterceptor

권한 부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한부여 결정 및 접근 제어 처리

 

'정리 > Spring' 카테고리의 다른 글

Spring Security _ 2탄 ( 이제 JWT를 곁들인.. )  (0) 2024.12.11
[Spring] DI & AOP  (0) 2021.09.24
[Spring] REST API  (0) 2021.09.09
[Spring] Spring MVC  (0) 2021.09.09
[Spring] Spring이란?  (0) 2021.09.09

오픈소스로 제공하고 있는 Elasticsearch + Kibana를 사용해 데이터 시각화 대시보드를 구성해 보고자 개인 서버에 설치하려는데 아래와 같은 오류가 발생했다.  일주일 전만 해도 패키지 설치 시, yum명령어로 설치했었는데 오늘 갑자기 오류가 발생해서 네트워크 문제 있은가 싶어 여러 설정을 변경해 봤는데... CentOS EOL 때문이었다.. 그것도 모르고 몇 시간 동안 삽질했는지😂😂😂

 

해당 오류를 어떻게 해결했는지 기록으로 남겨두려고 한다.

 

💡 혹시 네트워크 문제로 패키지 설치가 안되는 걸 수도 있으니, ping은 잘 되는지  `nmcli d` 명령어로 네트워크가 잘 설정되어 있는지 DNS 설정은 되어있는 지 등.. 먼저 확인해 보시길 추천드려요!

 

TroubleShooting

 

배경 : yum으로 패키지 설치 시 아래와 같은 오류 발생하면서 패키지 설치 불가

환경 : CentOS Linux release 7.9.2009

오류 내용 :

Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=7&arch=x86_64&repo=os&infra=stock error was 14: curl#6 - "Could not resolve host: mirrorlist.centos.org; Unknown error"

 

오류 사유 : 2024년 6월 30일부로 CentOS Linux가 지원 종료 (CentOS 7 EOL)
https://www.redhat.com/ko/topics/linux/centos-linux-eol

 

CentOS Linux의 End of Life (EOL) 필수 정보 요약

CentOS Linux 7의 지원 종료 (End of Life, EOL)는 2024년 6월 30일입니다. 이에 대한 준비 상황과 Red Hat의 마이그레이션 지원 방법을 알아봅니다

www.redhat.com

 

 

해결 방법 : 기존에 사용하던 레파지토리 URL변경

[root@instance-juyeon ~]# cd /etc/yum.repos.d

## 기존 yum 관련 레파지토리 백업 폴더 생성 및 이동
[root@instance-juyeon yum.repos.d]# mkdir backup
[root@instance-juyeon yum.repos.d]# mv CentOS-* backup

## 새 레파지토리 파일 생성
[root@instance-juyeon yum.repos.d]# vi CentOS-BASE.repo


[base]
name=CentOS-$releasever - Base

baseurl=http://centos.mirror.cdnetworks.com/7/os/x86_64
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#released updates
[updates]
name=CentOS-$releasever - Updates
baseurl=http://centos.mirror.cdnetworks.com/7/updates/x86_64
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras
baseurl=http://centos.mirror.cdnetworks.com/7/extras/x86_64
gpgcheck=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus
baseurl=http://centos.mirror.cdnetworks.com/7/centosplus/x86_64
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7



## yum cache 삭제
[root@instance-juyeon yum.repos.d]# yum clean all

 

위 코드와 같이 yum cache 까지 삭제 후, yum 명령어로 패키지 설치하면 정상 설치된다!

2401회(24.03.09) 리눅스 마스터 1급 필기시험을 보고 온 후기를 남겨보려고 한다.

 

매년 업무과 관련된 IT관련 자격증 2개 이상을 취득을 목표로 공부하고 있는데, 현재 회사에서도 리눅스 서버를 많이 접하기도 하고 해당 자격증 취득 시 업무에 많은 도움이 될 것 같아 자격증을 준비하게 되었다.

 

리눅스 사용경험은 2~3년 정도 되지만, 항상 그때그때 필요한 명령어를 구글링 해서 했기 때문에 정확한 동작과정, 설정파일 등.. 의 깊은 지식은 없는 상태라 이번에 자격증 준비하면서 리눅스 관련해서 많이 배우고 알게 된 거 같다.

 

✍🏻필기 공부 방법✍🏻

총 1달 정도 준비했으며, 이론 책 완독(3주) + 기출문제 풀이(1주) 로 준비했다.

 

먼저 3주간은 'CentOS 7으로 리눅스마스터 1급 정복하기' 책을 읽고 개인 서버에서 명령어를 입력해 보고 결괏값을 확인해 가는 방식으로 공부를 진행했다. 해당 책이 700페이지가 넘은 분량이라 노션에 정리하면서 공부했는데, 정리하는 시간도 오래 걸렸다..😂

 

[전자책] CentOS 7으로 리눅스마스터 1급 정복하기 - 예스24

리눅스마스터는 한국정보통신진흥협회에서 시행하고 있는 국가공인 자격증으로 리눅스 기반의 Desktop 활용 및 Server 운영 능력, 리눅스 시스템의 설계 개발 및 관리 능력, 리눅스 기반의 네트워

m.yes24.com

 

책 1회 완독을 한 후에는 단원별 문제풀이+ 최근 5년간 기출문제를 통해 부족한 개념에 대해 추가 보완하는 방식으로 진행했다.

회사일과 병행하면서 준비했던 터라, 평일에는 1~2시간 정도,  주말는 벼락치기하는 심정으로 4~5시간씩 공부했던 것 같다..ㅎㅎ

 

🐈필기 시험 후기🐈

시험장소는 리눅스마스터 시험 접수 할 때 지정하는 게 아니라 시험 10일 전에 접수사이트에서 고사장을 확인할 수 있었다.

나는 용산철도고등학교에서 시험을 봤고, 오후 1시 50분까지 입실이라 40분까지 고사장에 도착했다.

 

시험시작 10분 전에 감독관이 신분증 및 수험표 확인을 하면 A/B 유형이 나뉜 시험지를 나눠주신다. 인쇄상태만 잠깐 확인하고 시작 전까지 대기하다가 2시부터 100분간 진행되며, 시험시작 반이상이 지난 뒤부터 퇴실이 가능했다.

 

분명 다른 블로그 후기들을 보면 시험지 반출은 안되나, 수험표에 답안을 적어올 수 있게 해 주셨다는데 내가 시험본 감독관님은 수험표에 답을 적어가는 것도 안된다고 하셨다.😂 (가답안이 공개되어도 합불 여부를 미리 확인할 수 없다..ㅠㅠ) 

 

생각보다 단원별 문제(책) + 기출문제 (최근 ~ 13년도)에서 비슷한 문제가 10% 정도는 있었던 것 같다. 문제 다 풀고 마킹까지 끝내니 딱  시험시간 50분이 지나서 답안지/시험지 제출 후 바로 퇴실할 수 있었다.

 

시험 보고 난 3주 뒤인 3/29(금)에 시험결과가 나온다고 하니, 그때 다시 추가로 후기 작성할게요-!

 

 

+ 2024.04.01 후기 추가

 

2024.03.29(금) 오후 12시쯤 확인해 봤는데, 성적확인이 가능했다.!!

사실 시험 1주일 전만해도 자격증 준비량이 턱없이 부족해서 답이 없다고 생각했었는데, 마지막 1주일 동안 열심히 기출문제 푼 게 많은 도움이 된 것 같다.😆  더 높은 점수로 합격하지 못한 게 살짝 아쉽지만 실기땐 더 열심히 공부해서 시험 봐야지🤨

 

 

+ 2024.06.03 실기 후기 추가

2024.05.31(금)에 실기시험 결과를 확인할 수 있었다. 높은 점수로 합격한 건 아니지만, 그래도 실기 후기를 남겨보려고 한다.!

 

실기도 마찬가지로 필기때 공부했던 'CentOS 7으로 리눅스마스터 1급 정복하기'  책 + 이전 기출문제로 공부했으며, 명령어들을 하나씩 직접 가상환경에서 작성해 보고 결과를 확인해 보는 방식으로 공부했다.

 

리눅스마스터 2차 문제는 현재 공개되고 있지 않아서, 이전 기출문제를 바탕으로 공부하고 시험을보러갔었는데 체감상 기출문제(70%) + 새로 보는 유형(30%) 이였던 것 같다. 

 

시험시작 5분전에 알려주는 계정으로 가상환경에 접속하고 대기하는데, 버추얼박스 안에서 마우스 커서가 자꾸 사라져서 불편했다..

감사하게도 옆에 앉으신 분이 해당 설정 변경하는 방법을 알려주셨는데, 내 컴퓨터에는 설정을 변경해도 반영이 안돼서 시험 중간중간 마우스 사용하고 싶을 때 ctrl + alt + delete로 마우스를 계속 잡아줬다ㅠㅠㅠㅠㅠ ( 혹시 모르니 마우스 커서 설정 변경하는 방법 찾아보고 가세요! )

 

또, 작성한 답을 수정할 경우에는 수정할 답 마다 감독관님 싸인 이 필요하기 때문에 답안 작성 시 신중히 작성해야 한다. 수정 1개당 싸인 1개가 필요하므로 번거로울 수 있다😂

 

답은 수험표 뒤에 적어올 수 있으며, 시험 정답은 1주일 뒤에 KAIT 자격검정 사이트에서 확인할 수 있다.

 

이번 리눅스마스터 1급 자격증을 준비하면서 리눅스 관련해서 많이 배울 수 있었고, 현재 회사에서 업무 효율성도 높아진 거 같아 취득하길 잘한 거 같다는 생각이 든다. 

 

 

마지막으로 실기 준비 꿀팁으로

- find / man 명령어를 잘 사용하자!

- 대부분의 서비스(apache,mail, NFS.. 등등) 은 기본적으로 설치가 되어있는데 혹시 모르니 공부할 때는 한 번씩 직접 rpm, yum으로 설치해 보길 추천합니다!

 

이상 리눅스마스터 1급 자격증 시험 후기였습니다!

 

'후기 > 자격증' 카테고리의 다른 글

[AWS] SAA-C03 공부 방법 및 시험후기  (0) 2023.10.30
[SQLD] 45회 합격 후기 및 공부 방법  (0) 2022.06.03

AWS SAA-C03 오프라인 시험후기를 남겨보려고 한다.

 

✍🏻공부 방법✍🏻

나는 대략 3주 정도 준비했으며,

초반에는 유데미에 있는 강의를 한 번 다 듣고 덤프문제를 풀 생각이었으나 생각했던 것보다 범위도 많고 강의를 다 듣고 준비하기에는 시간이 너무 부족할 것 같아 덤프 문제에 해당하는 개념들을 찾아서 공부하는 방식으로 진행했다.

 

평일에는 1~2시간 정도 AWS Document 참고해서 각 내용들을 노션에 정리했고, 주말에는 덤프 문제를 풀었다. 덤프 문제도 약 700문제 이상이라 한 번 훑는데 오래 걸렸지만 뒤로 갈수록 중복되는 문제들도 있어서 나중에는 슉슉 풀어나갈 수 있었다.😅

 

내가 참고한 덤프 사이트는 examtopics 이고, 시험 접수할 때 기본 언어가 영어이지만 한국어도 추가로 선택할 수 있었기 때문에 나는 영어/한국어 두 가지 형태로 덤프문제를 풀었다.

 

https://www.examtopics.com/exams/amazon/aws-certified-solutions-architect-associate-saa-c02/

 

Free & Accurate Amazon AWS Certified Solutions Architect - Associate SAA-C02 Practice Questions | ExamTopics

Get ready to prepare like you’ve never prepared before As we often say at ExamTopics, work smarter not harder. You are about to see a study guide that took hours of hard collection work, expert preparation, and constant feedback. That’s why we know thi

www.examtopics.com

 

🐈 SAA-C03 오프라인 시험 후기 🐈

 

AWS 시험은 온/오프라인으로 볼 수 있는데 온라인으로 진행하면 주변 환경 정리 및 신분확인 절차 등이 까다롭다고 해서 맘 편하게 오프라인으로 보기로 했다.

 

온라인의 경우 원하는 날짜에 시험을 볼 수 있지만, 비교적 오프라인의 경우 시험 일정을 잡는게 쉽지 않았다.

나 같은 경우에는 평일에는 출근을 해야했기에.. 주말밖에 선택지가 없었다.

 

나는 강남에 있는 SRTC센터에서 시험을 봤고 신분 확인(신분증 & 신용카드), 동의서 작성, 소지한 물품 보관 및  검사 주머니 확인 및 전자기기 착용 여부 등..) 후 지정된 좌석에 앉아서 시험을 볼 수 있었다.  

 

문제를 다 풀고 시험 종료를 하고 나오면 개개인마다 결과통지 시간이 다르다고 하지만,  나 같은 경우에는 오후 8시쯤 결과를 메일로 받을 수 있었다.

 

 

 

 

 

 

구글 플레이스토어 앱 리뷰를 수집하여, 수집한 리뷰데이터 KOMORAN 라이브러리를 통해 명사만 추출하여 해당 앱들의 키워드를 worldCloud로 뿌려주는 기능을 개발하게 되었다. 

 

해당 기능을 개발하면서 겪었던 오류나 개발하면서 알게되었던 내용을 정리해보고자 한다.

 

 

1. 셀레니움 설치 전 Chrome 버전 확인

: Chrome 오른쪽 상단 ...  → 설정 → Chrome 정보에서 버전 확인 

2. Chrome Driver 설치하기

 

ChromeDriver - WebDriver for Chrome - Downloads

Current Releases If you are using Chrome version 115 or newer, please consult the Chrome for Testing availability dashboard. This page provides convenient JSON endpoints for specific ChromeDriver version downloading. For older version of Chrome, please see

chromedriver.chromium.org

ChromeDriver 홈페이지에서 위에서 확인한 자신의 Chrome버전과 맞는 Driver를 설치해줘야 한다.

 

 

3. pom.xml에 라이브러리 추가 (url : https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java)

<dependency>
	<groupId>org.seleniumhq.selenium</groupId>
	<artifactId>selenium-java</artifactId>
	<version>3.141.59</version>
</dependency>

 

4. 구글 플레이스토어 앱 리뷰 크롤링 하기

내가 수집하고자 하는 데이터는 다음과 같았다. ( 사용자 이름, 리뷰내용, 별점, 좋아요 수, 작성일자 )

 

구글플레이 스토어에서 앱에 대한 리뷰가 최대 3개밖에 보이지 않아서,  "모든 리뷰 보기" 버튼 클릭하는 코드를 추가해야했다.

 

 

F12(개발자 도구)를 누르면 해당 페이지의 HTML 코드를 볼 수 있어서 웹 페이지가 어떻게 구성되어 있는지 쉽게 파악할 수 있다.

 

4-1) 구글플레이스토어 웹 페이지 띄우기

public void getReivew() throws InterruptedException {
		
		System.setProperty(chromeDriverName, chromeDrvierPath);
		//크롬 드라이버 셋팅 (드라이버 설치한 경로 입력)
		
		ChromeOptions options = new ChromeOptions();
		options.addArguments("--no-sandbox")
			.addArguments("--disable-dev-shm-usage")
			.addArguments("--disable-blink-features=AutomationControlled");
		
		
		driver = new ChromeDriver(options);
		driver.get(url);    
 }

 

4-2) 리뷰 모두 보기 버튼을 클릭 했을 때, 동적 페이지 형식으로 되어있어서 모든 리뷰를 가져오기 위해서는 스크롤을 통해 맨 아래로 내리는 작업을 해줘야했다.

 

 scrollTo 함수를 사용했을 때, 리뷰 모달창이 아닌 뒤에 있는 페이지가 아래로 내려가 포커싱을 모달창에 잡아주는 과정에서 많은 시간을 소요했다.😂

 

WebElement prev_element = null ;
			
while(true) {
				
		WebElement element = driver.findElement(By.xpath("//*[@id=\"yDmH0d\"]/div[4]/div[2]/div/div/div/div/div[2]/div/div[2]/*[last()]"));
					
		if(element.equals(prev_element)) break;
					
		JavascriptExecutor.executeScript("arguments[0].scrollIntoView(true);", element);
		Thread.sleep(4000);
					
		prev_element = element;
}

i) 로딩된 리뷰 모달창에 사용자의 마지막 리뷰에 해당하는 xpath를 웹페이지에서 가져온 뒤,  해당 element 값이 prev_element 값과 동일하다면 웹 페이지에 모든 리뷰를 로딩했음을 의미하므로 종료

ii) JacascriptExecutor 를 이용해 해당 element 위치까지 스크롤를 내려주는 과정을 반복

 

[위 소스코드 결과]

 

 

4-3) 앱 리뷰 데이터 파싱해서, 내가 원하는 데이터로 가공해 적재하기

public void dataParsing(WebDriver driver, String appName) {
		List<WebElement> list = driver.findElements(By.className("RHo1pe"));

		DataMap map;
		
		try {
        
			for(int i=0;i<list.size();i++) {
				String[] str = list.get(i).getText().split("\\n");
				
				map = new DataMap();
				map.put("appName", appName);
				map.put("userName", str[0]);
				map.put("contents", str[3]);
		
				String score = list.get(i).findElement(By.xpath("//*[@id=\"yDmH0d\"]/div[4]/div[2]/div/div/div/div/div[2]/div/div[2]/div[38]/header/div[2]/div")).getAttribute("aria-label")
						.replaceAll("[^0-9]", "");
				map.put("score", Integer.parseInt(score.substring(score.length()-1)));
			
				String thumbsUp = str[4].replaceAll("[^0-9]", "");
				thumbsUp = thumbsUp.length()==0 ? "0" : thumbsUp;
				map.put("thumbsUp", Integer.parseInt(thumbsUp));

				map.put("cretDt", compareFormat.parse(str[2]));
				
				googlePlayStoreReviewMapper.insertReviewData(map);
			}
			
		} catch (ParseException e) {
			// catch
		}
	}

 

[앱 리뷰 데이터 파싱 결과]

 

 

## 2023-06-15 추가 

1) 매번 모든 리뷰를 파싱하면 비효율 적이므로, 최신 순으로 정렬 후 앱에 해당하는 마지막 리뷰일자를 찾아 그 이후 날짜에 해당하는 리뷰만 파싱해 올 수 있도록 코드를 수정했다.😊

'정리 > Java' 카테고리의 다른 글

ListIterator  (0) 2021.10.20

리눅스 환경에서 새로운 패키지를 설치할 때에 "yum을 통해 손쉽게 다운로드할 수 있지만,

현재 구축하려는 서버는 사내망에 있어서 인터넷이 불가능한 환경이라 yum명령어를 사용할 수 없었습니다..

 

yum을 사용하지 못할경우에는 설치하려는 rpm 파일을 하나씩 설치해야 하는데,

해당 rpm 파일을 설치하기 위해서는 이에 해당하는 의존성 패키지 또한 같이 설치해줘야 하는 번거로움이 존재합니다.

 

인터넷이 안 되는 서버에 패키지를 쉽게 설치하는 방법은

인터넷이 되는 환경의 서버에서 yumdownloader를 사용해서 필요한 패키지. rpm파일을 다운로드하는 것입니다.

 

yumdownloader 명령어에 --resolve 옵션을 추가하면 의존성 있는 패키지도 같이 다운로드할 수 있습니다.

# yumdownloader --resolve [패키지 명]

 

해당. rpm 파일이 다운됐으면 인터넷이 안 되는 서버에 옮긴 후,

# rpm -ivh [패키지 파일명].rpm

명령어를 통해, 패키지를 설치합니다.

 

하지만, 이상하게도 패키지를 설치하는 도중 다른 패키지가 없어 설치가 안된다는 에러 문구를 발견할 수 있는데.. 🤣

 

 

구글링 해본 결과,

yumdownloader 명령어를 통해 패키지를 다운로드할 때 이미 서버에 설치된 dependency의 경우 다운로드를 하지 않는다고 하더라구요..ㅠㅠㅠ

그래서 다운받을 rpm을 폐쇄망 서버에 옮겨서 설치했을 때 위와 같은 오류가 발생하는 것이었습니다..

 

그래서 저는 repotrack을 사용해 rpm패키지를 다운로드하였습니다.

repotrack은 yumdownloader --resolve과 매우 유사하지만, yumdownloader는 종속성을 해결할 때 이미 설치되어 있으면 해당 종속성을 설치하지 않고 건너뛰고 repotrack의 경우 모든 종속성을 다운로드할 수 있는 차이점이 있습니다.

 

repotrack -p [다운로드 파일 경로] [다운받을 패키지 명]

해당 명령어를 통해 모든 종속성 패키지와 함께 rpm을 다운받을 수 있었습니다!

 

repotrack으로도 위와 동일하게 패키지가 없어 설치가 안된다는 에러 문구가 보일 경우에는 

yum install [패키지 명] --downloadonly --downloaddir=[다운받을 경로]

로 설치해보세요!

Centos7에서 docker 최신버전 설치 후, docker run 명령어를 치면 아래와 같은 오류가 발생했다.

- 서버 정보 : Centos7

- 설치한 docker version : 18.09.3

docker: Error response from daemon: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/var/lib/docker/containers/241adecbff77a64f06883d9fe12c346d8928e8061f13ac5681e0b9990f71b3af/resolv.conf" to rootfs at "/etc/resolv.conf": possibly malicious path detected -- refusing to operate on /etc/resolv.conf: unknown.

 

 

구글링한 결과, Centos7버전대에서 해당 에러가 빈번히 발생하고 있는 걸 확인할 수 있었고, issue solved된 docker 버전으로 재설치했다.

 

해결방법

1) docker version : 19.03.1으로 재설치

# yum install docker-ce-19.03.1 \
                  docker-ce-cli-19.03.1 \
                  containerd.io
                  
                  
# systemctl start docker
# systemctl enable docker

--> 해당 버전으로 재설치했으나, 위와 동일한 오류 발생

 

 

2) docker version : 19.03.4으로 재설치

# yum install docker-ce-19.03.4 \
                  docker-ce-cli-19.03.4 \
                  containerd.io-1.3.7-3.1.el7
                  
                  
# systemctl start docker
# systemctl enable docker

--> 오류 해결!!

 

1. CMAK를 설치하게 된 이유

Kafka Manager는 GUI 기반 카프카 관리도구이다.

배치를 돌리면서 consumer하는 속도보다 producer 하는 속도가 더 빨라 설정된 kafka 메모리 초과로 배치가 제대로 실행되지 않았던 경험이있어, 카프카 모니터링 툴을 찾던 중 오픈소스인 CMAK를 설치하기로 했다.

 

카프카 모니터링 툴은 CMAK외에도 Kafdrop, Burrow 등 여러 오픈소스가 존재하지만,

설치가 간단하고, GUI로 토픽을 생성 및 변경할 수 있으면 좋을 것 같아 CMAK를 선택했다.

 

CMAK의 주요 기능

CMAK에서 제공하는 기능은 다음과 같다.

1. Kafka Cluster 관리

2. Consumer Lag 관리

3. GUI로 토픽 생성 및 변경

4. 파티션 추가

 

 

2. CMAK 설치

설치할 서버환경

  • CentOS 7

CMAK 설치 전, 기본 환경

  • JDK 11 이상
  • kafka 0.8 이상

 

1) tar.gz 파일 다운로드

# wget https://github.com/yahoo/CMAK/archive/refs/tags/3.0.0.6.tar.gz
# tar -zxvf 3.0.0.6.tar..gz

 

2) 현재 서버는 jdk1.8이지만 CMAK는 최소 JDK11 이상이기 때문에 sbt 파일을 수정했다.

- 1)에서 압축을 푼 파일로 이동하면 sbt 파일이 있다.

# vi sbt


[sbt]
-- 35번째 java_cmd 경로를 설치한 jdk11 위치로 변경

declare sbt_jar sbt_dir sbt_create sbt_version sbt_script sbt_new
declare sbt_explicit_version
declare verbose noshare batch trace_level

# declare java_cmd="java"
declare java_cmd="/usr/lib/jvm/jdk-11/bin/java" --이렇게!
declare sbt_launch_dir="$HOME/.sbt/launchers"
declare sbt_launch_repo

 

 

3) 빌드

# ./sbt clean dist

- 설치경로/CMAK-3.0.0.6/target/universal/cmak-3.0.0.6 생성된걸 확인 할 수 있다.

 

4) cofing 파일 수정

# cd /빌드한 파일 경로/confi/

-- application.conf 파일 수정

# Settings prefixed with 'kafka-manager.' will be deprecated, use 'cmak.' instead.
# https://github.com/yahoo/CMAK/issues/713
# kafka-manager.zkhosts="kafka-manager-zookeeper:2181"
kafka-manager.zkhosts="localhost:2181"
kafka-manager.zkhosts=${?ZK_HOSTS}
# cmak.zkhosts="kafka-manager-zookeeper:2181"
cmak.zkhosts="localhost:2181"
cmak.zkhosts=${?ZK_HOSTS}

 

5) 실행

# cd /app/kafkaManager/CMAK-3.0.0.6/target/univeral/cmak-3.0.0.6
# bin/cmak -Dhttp.port=9003 -java-home /usr/lib/jvm/jdk-11

- 자바버전을 11으로 잡아주기 위해 JAVA_HOME을 따로 잡아주었다.

- 기본 포트는 9000번이지만, 옵션을 통해 9003으로 변경해주었다. 

 

 

3. 실행결과 확인

 

 

1417번: 국회의원 선거

첫째 줄에 후보의 수 N이 주어진다. 둘째 줄부터 차례대로 기호 1번을 찍으려고 하는 사람의 수, 기호 2번을 찍으려고 하는 수, 이렇게 총 N개의 줄에 걸쳐 입력이 들어온다. N은 50보다 작거나 같

www.acmicpc.net

 

# 풀이 방법

국회의원 후보들 중 다솜이가 당선되어야 하기 때문에

현재 가장 많은 투표수를 가지고 있는 후보의 표를 다솜이에게 주면서 표수를 확인하는 방식으로 문제를 풀었습니다.

 

후보 수와 그 후보의 득표 수를 저장할 info 객체를 하나 만들어 다솜이를 제외한 후보의 정보를 우선순위 큐에 넣어주었습니다.

 

while문을 돌면서 우선순위 큐가 비었거나, 다솜이를 제외한 후보들 중 가장 많은 득표수를 가지고 있는 후보와 다솜이의 득표수를 비교해 다솜이가 더 많다면 더 이상 매수할 필요가 없으니 반복문을 종료했습니다.

반대로 다솜이보다 득표수가 많다면 그 후보의 표를 다솜이에게 주기 위해 -1 / +1을 하면서 다솜이의 득표수가 가장 많아질 때까지 반복해 주었습니다.

 

# 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Comparator;
import java.util.PriorityQueue;
 
public class Main_BJ_1417_국회의원선거 {
    
    static class Info{
        int number, vote;
 
        public Info(int number, int vote) {
            this.number = number;
            this.vote = vote;
        }
    }
    
    static int N;
 
    public static void main(String[] args) throws NumberFormatException, IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PriorityQueue<Info> pq = new PriorityQueue<>(new Comparator<Info>() {
 
            @Override
            public int compare(Info o1, Info o2) {
                return (o1.vote-o2.vote)*-1;
            }
        });
        
        N = Integer.parseInt(br.readLine());
        
        Info dasom = new Info(1, Integer.parseInt(br.readLine()));
        for(int i=2;i<=N;i++) {
            pq.add(new Info(i, Integer.parseInt(br.readLine())));
        }
        
        int count=0;
        while(true) {
            if(pq.isEmpty()||dasom.vote>pq.peek().vote) break;
            
            Info temp = pq.poll();
            dasom.vote=dasom.vote+1;
            pq.add(new Info(temp.number, temp.vote-1));
            count++;
        }
        
        System.out.println(count);
 
    }
 
}
cs

 

'문제 > 백준' 카테고리의 다른 글

백준 1918번 후위 표기식 [JAVA]  (0) 2022.04.26
백준 21608 상어 초등학교 [JAVA]  (0) 2021.12.01
백준 17143번 낚시왕 [JAVA]  (0) 2021.12.01
백준 13549번 숨바꼭질3 [JAVA]  (0) 2021.11.23
백준 3085번 사탕 게임 [JAVA]  (0) 2021.11.18

+ Recent posts