서블릿 객체는 쓰레드에 의해 호출되어진다. 그리고 여러 요청을 동시에 처리하기 위한 멀티 쓰레드는 필수적이다.

 

멀티 쓰레드를 지원하는 두 가지 방식이 있다.

 

1. 요청마다 생성

말 그대로 요청이 들어올 때마다 해당 요청을 처리하기 위한  쓰레드를 새로 생성한다.

이 경우 요청이 늘어남에 따라 컨텍스트 스위칭 비용이 무한정으로 늘어날 수 있다는 단점이 있다.

 

2. 쓰레드 풀

WAS 내부에서 사용할 일정 개수의 쓰레드들을 쓰레드 풀에 미리 생성한다.

요청이 들어오면 이 쓰레드 풀에서 쓰레드를 할당받아 요청을 처리한다.

사용이 완료된 쓰레드는 다시 풀에 반납한다.

쓰레드 풀의 쓰레드가 모두 사용중일 경우에 새로 들어오는 요청은 거절하거나 특정 수만큼 대기하도록 할 수 있다.

 

쓰레드 풀 방식을 사용하기 위해 최대 쓰레드 수를 얼마로 설정하는지가 중요하다. (톰캣은 최대 200개의 쓰레드가 디폴트 값이다.)

적절한 CPU 사용률을 유지할 수 있도록 MAX thread를 적절하게 설정해줘야 한다.

 

 

 

 

 

'김영한님 스프링 강의 정리 > 스프링 MVC' 카테고리의 다른 글

Servlet  (0) 2021.08.15
Web Server vs WAS(Web Application Server)  (0) 2021.08.15

서블릿이 없다면?

 

 

서블릿 없이 서버 개발자가 모든 기능을 전부 코딩해야 한다면 다음과 같은 로직들을 전부 구현해야 한다.

1. TCP/IP 대기, 소켓 연결

2. HTTP Request 메세지 받아서 분석

3. 비즈니스 로직 실행

4. DB 처리

5. HTTP Response 메세지 생성

6. TCP/IP를 통해 Response 전달

 

서블릿은 이러한 작업을 대신 처리하여 개발자의 업무 부담을 덜어준다 (3, 4는 직접 해야한다.)

 

서블릿 기능을 지원하는 WAS를 '서블릿 컨테이너'라고 칭한다.

서블릿 컨테이너는 서블릿 객체의 생명주기를 관리해주며 서블릿 객체는 싱글톤 패턴으로 관리된다.

서블릿 컨테이너는 동시 요청을 위한 멀티 쓰레드 처리를 지원한다.

 

WAS와 서블릿의 동작 방식

1. WAS가 HTTP Request를 받는다.

2. 요청 정보를 토대로 HttpServletRequest, HttpServletResponse 객체를 생성하여 서블릿 객체를 호출한다.

3. 서블릿은 비즈니르 로직 수행 후 HttpServletResponse를 적절하게 수정하여 반환한다.

4. WAS는 반환받은 정보를 토대로 HTTP Response를 반환한다.

 

 

 

Web Server

1. HTTP 기반으로 동작

2. 단순 '정적 리소스'를 제공

(ex : apache, nginx)

 

WAS

1. HTTP 기반으로 동작

2. Web Server 기능을 포함 (정적 리소스 제공)

3. 프로그램 코드를 실행하여 애플리케이션 로직 수행 (동적 처리 가능. Servlet, JSP, 스프링 MVC 등)

(ex : tomcat, jetty, undertow)

 

 

Web Server가 동적 기능까지 포함하는 경우도 있기 때문에 둘의 경계는 매우 모호하다.

 

그래도 굳이 정리하자면 

웹서버는 정적 리소스 담당, WAS는 정적 리소스 + 동적 기능 담당 정도로 생각할 수 있다.

 

WAS + DB만으로 시스템을 구성할 수는 있지만 이 경우 WAS의 역할이 너무 많아지고 서버 과부하의 위험이 있다.

정적 리소스 제공 역할을 수행하느라 정작 중요한 애플리케이션 로직의 수행에 어려움을 겪을 수 있다.

또한, WAS가 모든 역할을 맡게 되면 WAS가 죽었을 때 오류 화면을 노출해주는 것도 할 수 없게 된다.

 

이러한 문제들을 해결하기 위해 일반적으로 정적 리소스 처리만을 위한 웹서버를 따로 두고 WAS는 동적 기능에만 집중하는 식의 구성을 취한다.

 

이렇게 역할을 구분해놓으면 효율적인 리소스 관리가 가능하단 장점도 있다.

(정적 리소스 사용이 많으면 웹서버 증설, 애플리케이션 리소스 사용이 많으면 WAS 증설)

 

만약 화면을 띄워줄 필요 없이 API 통신으로 데이터만 넘겨주면 되는 경우라면 WAS만으로 구성하여도 문제될 것이 없다.

 

 

 

 

X to One 방식과 마찬가지로 fetch join을 사용하여 LAZY 로딩 없이 하나의 쿼리로 전부 가져올 수 있다.

 

 

    @GetMapping("/api/v3/orders")
    public List<OrderDto> ordersV3() {
        List<Order> orders = orderRepository.findAllWithItem();

        // order를 dto로 변환하여 반환
        List<OrderDto> result = orders.stream()
                .map(o -> new OrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
    
    @Data
    static class OrderDto {

        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;
        private List<OrderItemDto> orderItems;
        // DTO 안에 엔티티가 있는 것도 잘못된 설계
        // 이조차도 이런식으로 전부 DTO로 바꿔줘야 함

        public OrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();
            orderItems = order.getOrderItems().stream()
                    .map(orderItem -> new OrderItemDto(orderItem))
                    .collect(Collectors.toList());
        }
    }

    @Data
    static class OrderItemDto {

        private String itemName;  // 상품명
        private int orderPrice;  // 주문 가격
        private int count;  // 주문 수량

        public OrderItemDto(OrderItem orderItem) {
            itemName = orderItem.getItem().getName();
            orderPrice = orderItem.getOrderPrice();
            count = orderItem.getCount();
        }
    }
    
    .
    .
    .
    
    // OrderRepository 내부
    public List<Order> findAllWithItem() {
        return  em.createQuery(
                // JPQL distinct 기능 두 가지
                // 1. DB에 날리는 쿼리 수준에서 distinct 수행
                // 2. 그래도 걸러지지 않은 것들에 대해서는 o의 참조값을 확인하여 중복 제거
                "select distinct o from Order o" +
                        " join fetch o.member m" +
                        " join fetch o.delivery d" +
                        " join fetch o.orderItems oi" +
                        " join fetch oi.item i", Order.class)
                .getResultList();
    }
    
    
    

 

fetch join을 사용하지 않으면 order, orderItem, item 각각의 개수마다 쿼리가 1개씩 나가게 된다.

 

order를 조회하는 JPQL에서 연관된 모든 엔티티에 fetch join을 걸어주면 한 번의 쿼리만으로 모든 엔티티를 동시에 가져올 수 있다.

 

이때 distinct의 역할이 중요하다.

 

1:N 관계에서 join 쿼리를 수행할경우 row의 개수는 N쪽(여기선 orderItems)으로 맞춰진다. 즉, 1쪽(여기선 order)의 데이터가 중복해서 들어가게 된다.

다시말해서 똑같은 o가 여러 개가 나오는 것.

 

이러한 중복을 막기 위해 distinct를 사욯안다.

 

distinct는 두 가지 기능을 수행한다.

1. 일반적인 쿼리문의 distinct과 똑같은 기능

-> 하지만 위의 경우에서는 order만 같을 뿐 item 등의 정보는 모두 다르기 때문에 distinct에서 걸러지지 않는다 ( distinct는 row의 모든 column이 같을 때만 중복으로 판단 )

 

2. 똑같은 참조값을 가진 객체 중복 제거

-> 여기서 o의 중복이 제거된다.

 

 

 

 

이러한 방식은 성능을 줄여주는 데 큰 도움이 되지만 페이징 기능을 사용할 수 없다는 단점이 있다.

(원인에 대해서는 교재or강의를 참고하자 V3.1 강의의 16:30 ~ 23:30)

 

 

결론 : X To One 관계는 페치 조인을 해도 페이징에 아무런 영향이 없다. 따라서 X To One 관계는 페치 조인으로 한 번에 가져와서 쿼리 수를 줄이고, X To Many 관계는 hibernate: default_batch_fetch_size로 해결하자.

 

 

 

 

 

 

단순 쿼리를 직접 작성할 필요 없이 대신 작성해주는 것은 JPA의 큰 장점이다.

 

하지만 역설적이게도 이러한 장점이 성능 문제를 초래할 수 있다.

테이블을 이것저것 마구잡이로 조회해서 불필요하게 많은 쿼리가 나가는 것이다.

 

이러한 문제를 방지하고 최적의 성능을 이끌어내려면 여러가지 사항들을 신경쓰며 개발해야 한다.

아래는 이에 관하여 알아두면 좋은 내용들을 정리한 것이다.

 

 

 

 

 

(하이버네이트 모듈)

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

@Bean
Hibernate5Module hibernate5Module() {
	return new Hibernate5Module();
}

모든 연관관계에 대하여 LAZY 전략을 사용하는 것이 좋다.

LAZY 로딩을 사용한 객체는 직접적으로 사용하기 전까지는 프록시 객체를 담고 있게 된다.

 

이렇게 프록시 객체를 담고 있는 엔티티를 JSON으로 반환할 경우 제대로 된 값이 들어있지 않아 문제가 발생한다.

 

위 코드처럼 하이버네이트 모듈을 빈으로 등록해  놓는다면 이런 프록시 객체 엔티티에 대해서 모두 null값을 전달하도록 하는 기능을 기본 기능으로서 사용할 수 있다. (LAZY로딩된 엔티티들에 대한 로딩을 강제하는 옵션도 있음)

 

 

(@JsonIgnore)

양방향 연관관계를 가진 엔티티들에 대해 조회가 이뤄질 경우 무한루프를 돌면서 계속 서로를 조회하는 문제가 발생할 수 있다.

이를 방지하기 위해 양방향 연관관계 중 한 곳의 관계를 @JsonIgnore로 끊어서 JSON 전달을 막을 수 있다.

 

 

 

사실 위의 두 방법 (하이버네이트 모듈, @JsonIgnore)는 실무에서는 별로 사용될 일이 없다. 어차피 실무에서는 엔티티를 직접 전달하지 않고 항상 DTO를 통해서만 API 통신이 이뤄지기 때문이다.

 

 

 

 

(DTO를 통한 전달)

    @GetMapping("/api/v2/simple-orders")
    public List<SimpleOrderDto> ordersV2() {
        List<Order> orders = orderRepository.findAllByCriteria(new OrderSearch());
        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
    
    @Data
    static class SimpleOrderDto {
        private Long orderId;
        private String name;
        private LocalDateTime orderDate;
        private OrderStatus orderStatus;
        private Address address;  // 고객 주소 아니고 배송지 정보임

        public SimpleOrderDto(Order order) {
            orderId = order.getId();
            name = order.getMember().getName();  // Member LAZY 초기화
            orderDate = order.getOrderDate();
            orderStatus = order.getStatus();
            address = order.getDelivery().getAddress();  // Delivery LAZY 초기화
        }
    }

이렇게 inner class로 따로 만들어준 DTO 객체를 반환하면 내가 원하는 정보들만 담을 수 있으므로 위와 같은 상황을  고려하지 않아도 된다.

 

내가 필요한  엔티티에  Member, Delivery에 대해서는 getter를 사용할 때 쿼리가 날아가면서 프록시 객체가 아닌 진짜 객체가 들어간다.

 

하지만 이러한 방식은 order에 대한 쿼리 1번 + member, delivery에 대한 쿼리 각각 1번.  총 3번의 쿼리를 호출하게 되어 성능 저하를 초래할 수 있다.

 

 

 

 

(fetch join을 사용한 성능 최적화)

    // fetch 조인을 사용하여 발생하는 쿼리 수를 대폭 줄임
    @GetMapping("/api/v3/simple-orders")
    public List<SimpleOrderDto> orderV3() {
        List<Order> orders = orderRepository.findAllWithMemberDelivery();
        List<SimpleOrderDto> result = orders.stream()
                .map(o -> new SimpleOrderDto(o))
                .collect(Collectors.toList());
        return result;
    }
    
    .
    .
    .
    
    // OrderRepository 내부
    public List<Order> findAllWithMemberDelivery() {
        // order에 대한 한 번의 쿼리에서 member, delivery 를 한 번에 가져옴
        return em.createQuery(
                "select o from Order o" +
                        " join fetch o.member m" +
                        " join fetch o.delivery d", Order.class
        ).getResultList();
    }
    

레포지토리 내부에 페치 조인으로 order들을 조회하는 함수를 생성해 이를 사용한다.

 

fetch join을 사용해 JPQL을 작성하면 order에 대한 member, delivery를 별도의 쿼리로 조회하는 것이 아니라 order를 조회할 때 컬럼만 추가하여 함께 조회한다.

 

즉, order에 대한 쿼리 하나만 나가는 것이다.

 

 

 

 

www.acmicpc.net/problem/20056

 

20056번: 마법사 상어와 파이어볼

첫째 줄에 N, M, K가 주어진다. 둘째 줄부터 M개의 줄에 파이어볼의 정보가 한 줄에 하나씩 주어진다. 파이어볼의 정보는 다섯 정수 ri, ci, mi, si, di로 이루어져 있다. 서로 다른 두 파이어볼의 위치

www.acmicpc.net

문제

어른 상어가 마법사가 되었고, 파이어볼을 배웠다.

마법사 상어가 크기가 N×N인 격자에 파이어볼 M개를 발사했다. 가장 처음에 파이어볼은 각자 위치에서 이동을 대기하고 있다. i번 파이어볼의 위치는 (ri, ci), 질량은 mi이고, 방향은 di, 속력은 si이다. 위치 (r, c)는 r행 c열을 의미한다.

격자의 행과 열은 1번부터 N번까지 번호가 매겨져 있고, 1번 행은 N번과 연결되어 있고, 1번 열은 N번 열과 연결되어 있다.

파이어볼의 방향은 어떤 칸과 인접한 8개의 칸의 방향을 의미하며, 정수로는 다음과 같다.

7 0 1
6   2
5 4 3

마법사 상어가 모든 파이어볼에게 이동을 명령하면 다음이 일들이 일어난다.

  1. 모든 파이어볼이 자신의 방향 di로 속력 si칸 만큼 이동한다.
    • 이동하는 중에는 같은 칸에 여러 개의 파이어볼이 있을 수도 있다.
  2. 이동이 모두 끝난 뒤, 2개 이상의 파이어볼이 있는 칸에서는 다음과 같은 일이 일어난다.
    1. 같은 칸에 있는 파이어볼은 모두 하나로 합쳐진다.
    2. 파이어볼은 4개의 파이어볼로 나누어진다.
    3. 나누어진 파이어볼의 질량, 속력, 방향은 다음과 같다.
      1. 질량은 ⌊(합쳐진 파이어볼 질량의 합)/5⌋이다.
      2. 속력은 ⌊(합쳐진 파이어볼 속력의 합)/(합쳐진 파이어볼의 개수)⌋이다.
      3. 합쳐지는 파이어볼의 방향이 모두 홀수이거나 모두 짝수이면, 방향은 0, 2, 4, 6이 되고, 그렇지 않으면 1, 3, 5, 7이 된다.
    4. 질량이 0인 파이어볼은 소멸되어 없어진다.

마법사 상어가 이동을 K번 명령한 후, 남아있는 파이어볼 질량의 합을 구해보자.

입력

첫째 줄에 N, M, K가 주어진다.

둘째 줄부터 M개의 줄에 파이어볼의 정보가 한 줄에 하나씩 주어진다. 파이어볼의 정보는 다섯 정수 ri, ci, mi, si, di로 이루어져 있다.

서로 다른 두 파이어볼의 위치가 같은 경우는 입력으로 주어지지 않는다.

출력

마법사 상어가 이동을 K번 명령한 후, 남아있는 파이어볼 질량의 합을 출력한다.

제한

  • 4 ≤ N ≤ 50
  • 0 ≤ M ≤ N2
  • 1 ≤ K ≤ 1,000
  • 1 ≤ ri, ci ≤ N
  • 1 ≤ mi ≤ 1,000
  • 1 ≤ si ≤ 1,000
  • 0 ≤ di ≤ 7

예제 입력 1

4 2 1

1 1 5 2 2

1 4 7 1 6

예제 출력 1

8

예제 입력 2

4 2 2

1 1 5 2 2

1 4 7 1 6

예제 출력 2

8

예제 입력 3

4 2 3

1 1 5 2 2

1 4 7 1 6

예제 출력 3

0

예제 입력 4

7 5 3

1 3 5 2 4

2 3 5 2 6

5 2 9 1 7

6 2 1 3 5

4 4 2 4 2

예제 출력 4

9

 

 

 

 

 

 

 

풀이 .

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;

class Fireball {
    int r, c, m, s, d;
    boolean flag;
    Fireball(int r, int c, int m, int s, int d, boolean flag) {
        this.r = r;
        this.c = c;
        this.m = m;
        this.s = s;
        this.d = d;
        this.flag = flag;
    }
}

public class Main {
    static int n, m, k, ans;
    static boolean turnFlag;
    static ArrayList<Fireball>[][] map;
    static int[] rArr = {-1, -1, 0, 1, 1, 1, 0, -1};
    static int[] cArr = {0, 1, 1, 1, 0, -1, -1, -1};

    public static void calculate() {
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                if(map[i][j].isEmpty()) continue;
                for(Fireball f : map[i][j]) {
                    ans += f.m;
                }
            }
        }
    }

    public static void moveBall() {
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                if(map[i][j].isEmpty()) continue;

                for(int idx = 0; idx < map[i][j].size(); idx++) {
                    Fireball f = map[i][j].get(idx);
                    if(f.flag == turnFlag) continue;  // 이번 턴에 이동해서 온 것은 또 이동시키면 안 딤

                    int nr = f.r + rArr[f.d] * (f.s % n);
                    int nc = f.c + cArr[f.d] * (f.s % n);
                    if(nr > 0) nr %= n;
                    if(nc > 0) nc %= n;
                    if(nr < 0) nr = n - Math.abs(nr);
                    if(nc < 0) nc = n - Math.abs(nc);
                    // (f.s % n)을 곱하기 때문에 n-Math.abs()는 반드시 양수 보장

                    f.r = nr;
                    f.c = nc;
                    f.flag = turnFlag;
                    map[nr][nc].add(f);
                    map[i][j].remove(idx--);
                }
            }
        }
    }

    public static void sumAndDivide() {
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                int size = map[i][j].size();
                if(size < 2) continue;

                // sum
                int mSum = 0, sSum = 0;
                int oddCnt = 0, evenCnt = 0;
                for(Fireball f : map[i][j]) {
                    mSum += f.m;
                    sSum += f.s;
                    if(f.d % 2 != 0) oddCnt += 1;
                    if(f.d % 2 == 0) evenCnt += 1;
                }
                map[i][j].clear();  // 합쳐진 파이어볼들은 삭제해준다

                // divide
                int nm = mSum/5, ns = sSum/size, d = 1;
                if(nm == 0) continue;  // 질량이 0인 파이어볼은 사라진다
                if(oddCnt == size || evenCnt == size)
                    d = 0;  // 모두 홀수 or 모두 짝수이면 0,2,4,6
                for(int nd = d; nd <= 7; nd+=2) {
                    Fireball f = new Fireball(i, j, nm, ns, nd, turnFlag);
                    map[i][j].add(f);
                }
            }
        }
    }

    public static void process() {
        while(k-- > 0) {
            moveBall();
            sumAndDivide();
            turnFlag = !turnFlag;
        }
        calculate();
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken());
        m = Integer.parseInt(st.nextToken());
        k = Integer.parseInt(st.nextToken());
        map = new ArrayList[n][n];
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                map[i][j] = new ArrayList<>();
            }
        }

        for(int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            int r = Integer.parseInt(st.nextToken()) - 1;
            int c = Integer.parseInt(st.nextToken()) - 1;
            int m = Integer.parseInt(st.nextToken());
            int s = Integer.parseInt(st.nextToken());
            int d = Integer.parseInt(st.nextToken());
            Fireball f = new Fireball(r, c, m, s, d, true);
            map[r][c].add(f);
        }

        process();
        System.out.println(ans);
    }
}

 

이틀동안 해맸다.

 

s를 처음 입력받을 때부터 % n해서 입력받았는데 그게 문제였다. 처음부터 속도를 % 으로 받아버리면 파이어볼이 합쳐진 후에 나눠지는 과정에서 ns가 제대로 된 값을 얻지 못한다.

 

n == 4에서 s == 7, s == 1 일 경우 8/2 로 ns == 4가 되어야 하는데..

%으로 받으면 3, 1로 들어가기 때문에 ns == 2가 되는 문제였다.

 

 

근데 위 코드보다는 아래 방법이 더 나은 듯 하다.

 

1. Fireball을 담는 1차원 list 하나를 따로 둔다.

2. map[][]에는 int[]를 담고, 거기에는 "1."의 리스트의 인덱스를 담는다.

3. moveBall에서는 newMap[][]을 만들어 이동한 위치에 인덱스를 담은 후 map = newMap으로 덮어씌운다.

4. sumAndDivide에서는 newList를 만들어서 처리 후 덮어씌운다.

 

 

 

 

 

(딱히 JPA에 관련된 내용은 아니지만 JPA 강의에서 배운 내용이라 여기에 적는다)

 

 

@RequestBody, @ResponseBody를 통해 편리하게 API 통신을 할 수 있다.

 

이때, 엔티티 객체를 직접적으로 받고, 반환해줘도 단순히 기능을 구현하는 데는 별 문제가 없다. 

하지만 이러한 방식은 좋지 않다. "반드시" 엔티티를 그대로 사용하지 말고 DTO를 통해 엔티티의 정보를 전달해야한다.

 

DTO를 반드시 사용해야 하는 이유는 다음과 같다.

 

 

1. DTO를 사용하지 않을 경우 엔티티의 변경에 의해 API 스펙이 변경될 수 있다.

 

엔티티와 API가 일대일 대응의 관계를 가진다면 엔티티에 수정이 일어날 때마다 API 스펙을 일일히 변경해줘야한다.

이는 매우 번거로운 작업이며 컴파일 에러로 이를 감지할 수 없기 때문에 에러 원인을 찾기가 어렵다.

(DTO를 사용하면 DTO -> 엔티티 과정에서 컴파일 에러가 발생되므로 엔티티의 변경사항을 반드시 파악할 수 있다)

 

 

2. 하나의 엔티티에 대해서 API는 여러 개가 존재할 수 있다.

 

각각의 API가 요구하는 엔티티에 대한 데이터는 모두 다를 확률이 높다.

이때, 그냥 엔티티의 모든 정보를 넘겨줘버린다면 필요없는 데이터까지 받긴 하지만 필요한 건 전부 받은 셈이니 기능 동작에는 문제가 없을 것이다.

 

하지만 PW처럼 보안상 감추어야 할 정보까지 모두 JSON으로 함께 넘어가기 때문에 보안 문제가 발생할 수 있다.

엔티티 측에서 컬럼에 @JsonIgnore를 사용해 JSON 전달을 막을 수는 있지만 이는 엔티티가 API 스펙에 의존성을 갖게 되므로 좋지 않다.

 

유지보수가 복잡해질 뿐 아니라 다른 API에 대해서는 그떄그떄 또 변경을 해줘야 하는 쓸 데 없는 번거로움이 발생한다.

 

 

3. 엔티티를 그대로 넘겨줄 경우, 엔티티가 가진 정보 외의 것들은 넘겨주지 못한다.

DTO를 사용하면 엔티티의 정보 외에 추가적으로 필요한 정보도 함께 넘겨줄 수 있다.

 

 

 

 

 

 

 

 

www.acmicpc.net/problem/19237

 

19237번: 어른 상어

첫 줄에는 N, M, k가 주어진다. (2 ≤ N ≤ 20, 2 ≤ M ≤ N2, 1 ≤ k ≤ 1,000) 그 다음 줄부터 N개의 줄에 걸쳐 격자의 모습이 주어진다. 0은 빈칸이고, 0이 아닌 수 x는 x번 상어가 들어있는 칸을 의미

www.acmicpc.net

문제

청소년 상어는 더욱 자라 어른 상어가 되었다. 상어가 사는 공간에 더 이상 물고기는 오지 않고 다른 상어들만이 남아있다. 상어에는 1 이상 M 이하의 자연수 번호가 붙어 있고, 모든 번호는 서로 다르다. 상어들은 영역을 사수하기 위해 다른 상어들을 쫓아내려고 하는데, 1의 번호를 가진 어른 상어는 가장 강력해서 나머지 모두를 쫓아낼 수 있다.

N×N 크기의 격자 중 M개의 칸에 상어가 한 마리씩 들어 있다. 맨 처음에는 모든 상어가 자신의 위치에 자신의 냄새를 뿌린다. 그 후 1초마다 모든 상어가 동시에 상하좌우로 인접한 칸 중 하나로 이동하고, 자신의 냄새를 그 칸에 뿌린다. 냄새는 상어가 k번 이동하고 나면 사라진다.

각 상어가 이동 방향을 결정할 때는, 먼저 인접한 칸 중 아무 냄새가 없는 칸의 방향으로 잡는다. 그런 칸이 없으면 자신의 냄새가 있는 칸의 방향으로 잡는다. 이때 가능한 칸이 여러 개일 수 있는데, 그 경우에는 특정한 우선순위를 따른다. 우선순위는 상어마다 다를 수 있고, 같은 상어라도 현재 상어가 보고 있는 방향에 따라 또 다를 수 있다. 상어가 맨 처음에 보고 있는 방향은 입력으로 주어지고, 그 후에는 방금 이동한 방향이 보고 있는 방향이 된다.

모든 상어가 이동한 후 한 칸에 여러 마리의 상어가 남아 있으면, 가장 작은 번호를 가진 상어를 제외하고 모두 격자 밖으로 쫓겨난다.

<그림 1>

우선 순위상어 1상어 2상어 3상어 4↑↑↑↑↓↓↓↓←←←←→→→→

↓ ← ↑ → ↓ → ← ↑ → ← ↓ ↑ ← → ↑ ↓
→ ↑ ↓ ← ↓ ↑ ← → ↑ → ← ↓ ← ↓ → ↑
← → ↓ ↑ ← → ↑ ↓ ↑ ← ↓ → ↑ → ↓ ←
→ ← ↑ ↓ → ↑ ↓ ← ← ↓ ↑ → ↑ → ↓ ←

<표 1>

<그림 1>은 맨 처음에 모든 상어가 자신의 냄새를 뿌린 상태를 나타내며, <표 1>에는 각 상어 및 현재 방향에 따른 우선순위가 표시되어 있다. 이 예제에서는 k = 4이다. 왼쪽 하단에 적힌 정수는 냄새를 의미하고, 그 값은 사라지기까지 남은 시간이다. 좌측 상단에 적힌 정수는 상어의 번호 또는 냄새를 뿌린 상어의 번호를 의미한다.

<그림 2>

<그림 3>

<그림 2>는 모든 상어가 한 칸 이동하고 자신의 냄새를 뿌린 상태이고, <그림 3>은 <그림 2>의 상태에서 한 칸 더 이동한 것이다. (2, 4)에는 상어 2과 4가 같이 도달했기 때문에, 상어 4는 격자 밖으로 쫓겨났다.

<그림 4>

<그림 5>

<그림 4>은 격자에 남아있는 모든 상어가 한 칸 이동하고 자신의 냄새를 뿌린 상태, <그림 5>는 <그림 4>에서 한 칸 더 이동한 상태를 나타낸다. 상어 2는 인접한 칸 중에 아무 냄새도 없는 칸이 없으므로 자신의 냄새가 들어있는 (2, 4)으로 이동했다. 상어가 이동한 후에, 맨 처음에 각 상어가 뿌린 냄새는 사라졌다.

이 과정을 반복할 때, 1번 상어만 격자에 남게 되기까지 몇 초가 걸리는지를 구하는 프로그램을 작성하시오.

입력

첫 줄에는 N, M, k가 주어진다. (2 ≤ N ≤ 20, 2 ≤ M ≤ N2, 1 ≤ k ≤ 1,000)

그 다음 줄부터 N개의 줄에 걸쳐 격자의 모습이 주어진다. 0은 빈칸이고, 0이 아닌 수 x는 x번 상어가 들어있는 칸을 의미한다.

그 다음 줄에는 각 상어의 방향이 차례대로 주어진다. 1, 2, 3, 4는 각각 위, 아래, 왼쪽, 오른쪽을 의미한다.

그 다음 줄부터 각 상어의 방향 우선순위가 상어 당 4줄씩 차례대로 주어진다. 각 줄은 4개의 수로 이루어져 있다. 하나의 상어를 나타내는 네 줄 중 첫 번째 줄은 해당 상어가 위를 향할 때의 방향 우선순위, 두 번째 줄은 아래를 향할 때의 우선순위, 세 번째 줄은 왼쪽을 향할 때의 우선순위, 네 번째 줄은 오른쪽을 향할 때의 우선순위이다. 각 우선순위에는 1부터 4까지의 자연수가 한 번씩 나타난다. 가장 먼저 나오는 방향이 최우선이다. 예를 들어, 우선순위가 1 3 2 4라면, 방향의 순서는 위, 왼쪽, 아래, 오른쪽이다.

맨 처음에는 각 상어마다 인접한 빈 칸이 존재한다. 따라서 처음부터 이동을 못 하는 경우는 없다.

출력

1번 상어만 격자에 남게 되기까지 걸리는 시간을 출력한다. 단, 1,000초가 넘어도 다른 상어가 격자에 남아 있으면 -1을 출력한다.

예제 입력 1 복사

5 4 4

0 0 0 0 3

0 2 0 0 0

1 0 0 0 4

0 0 0 0 0

0 0 0 0 0

4 4 3 1

2 3 1 4

4 1 2 3

3 4 2 1

4 3 1 2

2 4 3 1

2 1 3 4

3 4 1 2

4 1 2 3

4 3 2 1

1 4 3 2

1 3 2 4

3 2 1 4

3 4 1 2

3 2 4 1

1 4 2 3

1 4 2 3

예제 출력 1

14

문제에 나온 그림과 같다.

예제 입력 2

4 2 6

1 0 0 0

0 0 0 0

0 0 0 0

0 0 0 2

4 3

1 2 3 4

2 3 4 1

3 4 1 2

4 1 2 3

1 2 3 4

2 3 4 1

3 4 1 2

4 1 2 3

예제 출력 2

26

예제 입력 3

5 4 1

0 0 0 0 3

0 2 0 0 0

1 0 0 0 4

0 0 0 0 0

0 0 0 0 0

4 4 3 1

2 3 1 4

4 1 2 3

3 4 2 1

4 3 1 2

2 4 3 1

2 1 3 4

3 4 1 2

4 1 2 3

4 3 2 1

1 4 3 2

1 3 2 4

3 2 1 4

3 4 1 2

3 2 4 1

1 4 2 3

1 4 2 3

예제 출력 3

-1

예제 입력 4

5 4 10

0 0 0 0 3

0 0 0 0 0

1 2 0 0 0

0 0 0 0 4

0 0 0 0 0

4 4 3 1

2 3 1 4

4 1 2 3

3 4 2 1

4 3 1 2

2 4 3 1

2 1 3 4

3 4 1 2

4 1 2 3

4 3 2 1

1 4 3 2

1 3 2 4

3 2 1 4

3 4 1 2

3 2 4 1

1 4 2 3

1 4 2 3

예제 출력 4

-1

 

 

 

 

 

 

풀이 .

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

/**
 *
 * 다른 상어의 냄새가 있는 곳으로는 가지 않는다.
 * 즉, 여러 상어가 한 칸에서 만나는 경우는 그 칸이 빈 칸으로 존재했을 경우에만 가능하다.
 *
 * 냄새의 카운트는 그 칸에 도착해서 냄새를 남긴 순간도 포함해서 센다.
 * 내 냄새가 있는 칸으로 이동한 경우에는 그냥 있던 냄새를 덮어버리고 다시 K부터  카운트 시작
 *
 * 결국 두 작업을 반복해서 하는 것.
 * 1. 냄새를 남긴다
 * 2. 이동한다
 *
 * 상어를 담는 배열과 냄새를 담는 배열 두 개를 따로 사용하는 게 좋을 듯
 *
 */

class Smell {
    int num, time;
    Smell(int num, int time) {
        this.num = num;
        this.time = time;
    }
}

class Shark {
    int r, c, num, dir;
    Shark(int r, int c, int num) {
        this.r = r;
        this.c = c;
        this.num = num;
    }
}

public class Main {
    static int n, m, k, sharkNum, ans;
    static Shark[] sharkArr;
    static Smell[][] smell;
    static int[][] map;
    static int[][][] prior;  // 각 상어의 방향에 대한 우선순위  [num][현재방향][우선순위] == 방향
    static int[] rArr = {0, -1, 1, 0, 0};  // 0상하좌우
    static int[] cArr = {0, 0, 0, -1, 1};

    public static void smelling() {
        for(Shark s : sharkArr) {
            if(s == null) continue;
            int r = s.r, c = s.c, num = s.num;
            if(smell[r][c] == null) {  // 빈 칸으로 들어옴
                smell[r][c] = new Smell(num, k);
            }else {  // 내 냄새가 있는 칸으로 들어옴
                smell[r][c].time = k;
            }
        }
    }

    public static void runOutOfSmellTime() {
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                if(smell[i][j] != null) {
                    if(smell[i][j].time == 1) {
                        smell[i][j] = null;
                    }else {
                        smell[i][j].time -= 1;
                    }
                }
            }
        }
    }

    public static void moveShark() {
        // 같은 칸에 여러 상어 오면 제거하는 로직도 여기서 수행
        for(Shark s : sharkArr) {
            if(s == null) continue;

            boolean find = false;
            int r = s.r, c = s.c, num = s.num, dir = s.dir;

            // 빈 칸 체크
            for(int i = 1; i <= 4; i++) {
                int nd = prior[num][dir][i];  // 현 상태에서 i번째 우선운위를 갖는 방향
                int nr = r + rArr[nd];
                int nc = c + cArr[nd];

                if(-1 < nr && nr < n && -1 < nc && nc < n) {
                    if(smell[nr][nc] == null) {  // 남의 냄새가 없는 빈 칸
                        // 이미 빈 칸에 다른 상어가 있으면 대결을 해야 함
                        if(map[nr][nc] == 0) {  // 아무 상어도 없음
                            map[r][c] = 0; map[nr][nc] = num;
                            s.r = nr; s.c = nc; s.dir = nd;
                        }else {  // 다른 상어를 만남
                            if(map[nr][nc] < num) {   // 미리 와있던 상어가 더 쏌
                                sharkArr[num] = null;  // 나 죽음
                                sharkNum -= 1;
                                map[r][c] = 0;
                            }else {
                                sharkArr[map[nr][nc]] = null;  // 미리 와있던 상어 죽음
                                sharkNum -= 1;
                                map[r][c] = 0; map[nr][nc] = num;
                                s.r = nr; s.c = nc; s.dir = nd;
                            }
                        }
                        find = true;
                        break;
                    }
                }
            }
            if(find) continue;

            // 빈 칸이 없는 경우 자기 냄새 있는 칸 체크
            for(int i = 1; i <= 4; i++) {
                int nd = prior[num][dir][i];
                int nr = r + rArr[nd];
                int nc = c + cArr[nd];

                if(-1 < nr && nr < n && -1 < nc && nc < n) {
                    // 여기선 대결을 고려할 필요가 없다
                    // 다른 상어의 냄새가 있는 곳으로 가는 경우는 없기 때문
                    if(smell[nr][nc].num == num) {  // 내 냄새인 경우
                        map[r][c] = 0; map[nr][nc] = num;
                        s.r = nr; s.c = nc; s.dir = nd;
                        break;
                    }
                }
            }
        }
    }

    public static void process() {
        while(sharkNum != 1) {
            smelling();  // 냄새 남긴다
            moveShark();  // 다음 칸으로 이동한다
            runOutOfSmellTime();  // smell time을 줄인다
            ans += 1;
            if(ans > 1000) return;
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken());
        m = Integer.parseInt(st.nextToken());
        k = Integer.parseInt(st.nextToken());
        sharkNum = m;

        sharkArr = new Shark[m+1];
        map = new int[n][n];
        smell = new Smell[n][n];
        for(int i = 0; i < n; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j < n; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
                if(map[i][j] != 0) {
                    sharkArr[map[i][j]] = new Shark(i, j, map[i][j]);  // idx == 상어의 번호
                }
            }
        }

        // 상어들의 처음 방향 일괄 입력
        st = new StringTokenizer(br.readLine());
        for(int i = 1; i <= m; i++) {
            sharkArr[i].dir = Integer.parseInt(st.nextToken());
        }

        // 상어들의 방향 우선 순위
        prior = new int[m+1][5][5];  // 행 번호를 상어 번호와 맞춰주기 위해 m+1행
        for(int num = 1; num <= m; num++) {
            for(int i = 1; i <= 4; i++) {
                st = new StringTokenizer(br.readLine());
                for(int j = 1; j <= 4; j++) {
                    prior[num][i][j] = Integer.parseInt(st.nextToken());
                }
            }
        }

        process();  // 1001초가 되면 강제 return 하여 -1 출력
        System.out.println(ans > 1000 ? -1 : ans);  // 1000초가 넘어도 다른 상어가 격자에 남아 있으면 -1을 출력한다.
    }
}

 

빡빡빡구현 문제.

 

특별히 어려운 내용은 없지만 그냥 구현 자체가 어렵다.

 

조건도 많아서 까다로웠다.

 

 

 

 

+ Recent posts