이러한 문제를 방지하고 최적의 성능을 이끌어내려면 여러가지 사항들을 신경쓰며 개발해야 한다.
아래는 이에 관하여 알아두면 좋은 내용들을 정리한 것이다.
(하이버네이트 모듈)
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를 조회할 때 컬럼만 추가하여 함께 조회한다.
청소년 상어는 더욱 자라 어른 상어가 되었다. 상어가 사는 공간에 더 이상 물고기는 오지 않고 다른 상어들만이 남아있다. 상어에는 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복사
544
00003
02000
10004
00000
00000
4431
2314
4123
3421
4312
2431
2134
3412
4123
4321
1432
1324
3214
3412
3241
1423
1423
예제 출력 1
14
문제에 나온 그림과 같다.
예제 입력 2
426
1000
0000
0000
0002
43
1234
2341
3412
4123
1234
2341
3412
4123
예제 출력 2
26
예제 입력 3
541
00003
02000
10004
00000
00000
4431
2314
4123
3421
4312
2431
2134
3412
4123
4321
1432
1324
3214
3412
3241
1423
1423
예제 출력 3
-1
예제 입력 4
5410
00003
00000
12000
00004
00000
4431
2314
4123
3421
4312
2431
2134
3412
4123
4321
1432
1324
3214
3412
3241
1423
1423
예제 출력 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을 출력한다.
}
}