Q. 우리는 nxn의 크기인 #과 공백(1과 0)으로 이루어진 지도 2가지를 바탕으로
원래의 지도가 무엇인지를 파악하고 싶다
이때, input으로 지도의 크기를 나타내는 n, 길이가 n인 arr1와 arr2가 들어온다
길이가 5인 이진수를 구하고 싶은데 Integer.toBinaryString(8)을 하면 100 길이가 3인 이진수가 나온다
→ 길이가 n으로 일정한 이진수를 구하기 위해서는 길이가 짧은 경우 문자열의 앞에 0을 더해준다
→ for 문을 이용해서 추가할 수 있지만 .repeat() 이용해보자
// .repeat는 byte의 형태로 연산을 한 후 이를 Sting(반복횟수, 글자)를 통해 문자열로 반환한다
// 문자열 길이가 1인 경우 Arrays.fill을 이용해서 byte 코드 복사
if (len == 1) {
final byte[] single = new byte[count];
Arrays.fill(single, value[0]);
return new String(single, coder);
}
...
// 문자열의 길이가 1보다 큰 경우는 repeat횟수만큼 for문을 돌아서 byte 코드로 복사
final int limit = len * count;
final byte[] multiple = new byte[limit];
System.arraycopy(value, 0, multiple, 0, len);
int copied = len;
for (; copied < limit - copied; copied <<= 1) {
System.arraycopy(multiple, 0, multiple, copied, copied);
}
System.arraycopy(multiple, 0, multiple, copied, limit - copied);
return new String(multiple, coder);
→ 길이가 1인 경우는 Arrays.fill을 이용하기 때문에 시간 복잡도가 작을 수 있지만 길이가 1보다 큰 경우는 동일하게 반복하는 횟수만큼 for문을 돌리므로 시간 복잡도가 비슷하다고 볼 수 있다.(1) 문자열로 변환한 후 길이가 n보다 작으면 차이나는 횟수만큼 0을 앞부분에 더해주기
String solution1(int n, int[] arr1, int[] arr2) {
String answer = "";
String[] sArr1 = new String[n];
String[] sArr2 = new String[n];
for (int i = 0; i < n; i++) {
String binStr1 = Integer.toBinaryString(arr1[i]);
String binStr2 = Integer.toBinaryString(arr2[i]);
sArr1[i] = "0".repeat(n - binStr1.length()) + binStr1;
sArr2[i] = "0".repeat(n - binStr2.length()) + binStr2;
}
....
}
(2) 만큼 0을 앞에 붙이기
9의 경우를 예로 들면 이고 4자리의 이진수로 1001로 나타낼 수 있다
18의 경우 이고 5자리의 이진수 10010으로 나타낼 수 있다.
→ 의 정수부분 만큼의 자리수를 가진 이진수를 생성할 수 있다.
String solution2(int n, int[] arr1, int[] arr2) {
String answer = "";
String[] sArr1 = new String[n];
String[] sArr2 = new String[n];
for (int i = 0; i < n; i++) {
int move1 = (int) (n-1 - Math.floor(Math.log(arr1[i])/Math.log(2)));
int move2 = (int) (n-1 - Math.floor(Math.log(arr2[i])/Math.log(2)));
sArr1[i] = "0".repeat(move1) + Integer.toBinaryString(arr1[i]);
sArr2[i] = "0".repeat(move2) + Integer.toBinaryString(arr2[i]);
}
...
}
// (1) 과 계산 속도가 비슷한데 굳이 이렇게까지 해야하나 싶은 생각이 들기도 한다...
0을 2로 나눈 나머지를 구하는 것을 100번 반복해도 0이, 마찬가지로 1을 2로 나눈 나머지를 100번 반복해도 1이 나온다.
→ 2보다 작으면 자기 자신이 그대로 나오기 때문에 길이를 맞춰줄 필요 없이 나머지 연산을 n번 반복하면 된다.
public String[] solution(int n, int[] arr1, int[] arr2) {
String[] answer = new String[n];
for (int i = 0; i < arr1.length; i++) {
String line = "";
int num1 = arr1[i];
int num2 = arr2[i];
for (int j = 0; j < arr1.length; j++) {
if(num1 % 2 + num2 % 2 == 0){// 두 수의 나머지가 모두 0일 조건
line = " " + line;
} else{
line = "#" + line;
}
num1 /=2;
num2 /=2;
}
...
}
: 비트 단위(이진수)로 논리 연산을 하거나 비트 단위로 왼쪽이나 오른쪽으로 이동시킬 때 사용하는 연산자
비트 연산자 종류
비트 연산자 | 설명 |
---|---|
& | 대응되는 비트가 모두 1이면 1을 반환함. (비트 AND 연산) |
^ | 대응되는 비트가 서로 다르면 1을 반환함. (비트 XOR 연산) |
~ | 비트를 1이면 0으로, 0이면 1로 반전시킴. (비트 NOT 연산) |
<< | 지정한 수만큼 비트들을 전부 왼쪽으로 이동시킴. (left shift 연산) → 이것을 활용하면 2의 n제곱 값을 가장 빠르게 구할 수 있음 |
>> | 부호를 유지하면서 지정한 수만큼 비트를 전부 오른쪽으로 이동시킴. (right shift 연산) |
비트 연산자 중 OR(|)의 경우 대응되는 비트 중 하나라도 1이면 1을, 모두 0이면 0을 반환한다.
String[] solution(int n, int[] arr1, int[] arr2) {
var answer = new String[n];
for (int i = 0; i < n; i++) {
answer[i] = Integer.toBinaryString(arr1[i] | arr2[i])
.replace("1","#").replace("0", " ");
// 들어온 두 수를 비트 연산자를 통해서 비교하여 모두 0이면 0, 하나라도 1이면 1을 반환하게 함
answer[i] = " ".repeat(n - answer[i].length()) + answer[i];
// 길이가 맞지 않는 경우 (ex. 101과 111을 계산한 경우) 길이를 맞춰줌
}
return answer;
}
폴더 구조
main
└ controller
└ HospitalController.java
└ domain
└ dto
└ HospitalDto.java
└ Hospital.java
└ HospitalBoardApplication.java
└ resources
└ templates
└ layouts
└ header.mustache
└ footer.mustache
└ list.mustache
└ footer.mustache
(1) HospitalController
@Controller
@RequestMapping("/hospital")
@Slf4j // 로깅을 도와주는 anotation
public class HospitalController {
private final HospitalRepository hospitalRepository;
public HospitalController(HospitalRepository hospitalRepository) {
this.hospitalRepository = hospitalRepository;
}
// @Autowired
// HospitalRepository hospitalRepository;
// 를 이용해서도 DI를 할 수 있지만 요즘은 private final과 constructor를 이용하여
// DI를 하는 추세
@GetMapping("")
public String mainDisplay(){
return "redirect:/hospital/list";
// 첫 페이지(.../hospital)를 list 형태로 표현하고 싶음 -> list페이지로 redirect
}
//불러온 정보를 …/hospital/lists url에서 게시판 형태로 볼 수 있게 해준다.
@GetMapping(value = "/list")
public String hospitalList(Model model){
// view 부분으로 정보를 넘겨주고 싶은 경우는 Model을 이용해야 함
List<Article> hospitalList = hospitalRepository.findAll();
// DB에 있는 값을 모두 조회하여 리스트로 반환한다
model.addAttribute("hospitals", hospitalList);
// model에 hospitals로 넣어준다
// 이후 view 부분(.mustache)에서 {{hospitals}}의 형태로 저장된 값들을 불러올 수 있다.
return "list";
}
...
}
@Autowired: 필드 주입(Field Injection) - 필드에 어노테이션을 붙여주어 의존성을 주입
@Component
public class MadExample {
@Autowired
private HelloService helloService;
}
private final: 생성자 기반 DI(Constructor Based DI)
@Component
public class MadExample {
// final로 선언할 수 있는 보너스
private final HelloService helloService;
// 단일 생성자인 경우는 추가적인 어노테이션이 필요 없다.
public MadExample(HelloService helloService) {
this.helloService = helloService;
}
}
setter: 수정자 주입(setter injection) - setter 또는 이름이 다르더라도 setter와 동일한 기능을 하는 메소드로 의존성 주입
@Component
public class MadExample {
private HelloService helloService;
@Autowired
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
}
생성자 기반 DI를 더 선호 하는 이유
StackOverflowError
가 뜨면서 종료된다.BeanCurrentlyInCreationException
이 발생하여 애플리케이션이 구동 되지도 않아 어떤 오류가 발생 했는지를 알 수 있다.(2) Hospital
@Entity
@Table(name="nation_wide_hospitals")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Hospital {
// DB에 저장된 정보 중 내가 원하는 정보만 필드로 생성
// id, 관리번호, 영업 상태, 전화번호, 도로명 주소, 병원명, 병원 종류
// 의료인 수, 병상 수, 총 면적을 가져옴
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String managementNumber;
private int businessStatus;
private String phone;
private String roadNameAddress;
private String hospitalName;
private String businessTypeName;
private int healthcareProviderCount;
private int patientRoomCount;
private int totalNumberOfBeds;
// @Column을 사용하지 않아도 total_number_of_beds에 매칭된다.
@Column(name = "total_area_size")
private float totalAreaSize;
// 이 중 게시판에서는
// id, 병원 이름, 병원 종류, 도로명 주소, 의료인 수, 입원실 수, 병상수를
// 보여줄 예정임
public Hospital(int id, String roadNameAddress, String hospitalName, String businessTypeName, int healthcareProviderCount, int patientRoomCount, int totalNumberOfBeds) {
this.id = id;
this.roadNameAddress = roadNameAddress;
this.hospitalName = hospitalName;
this.businessTypeName = businessTypeName;
this.healthcareProviderCount = healthcareProviderCount;
this.patientRoomCount = patientRoomCount;
this.totalNumberOfBeds = totalNumberOfBeds;
}
}
: DB에서 사용할 테이블과 엔티티를 매핑한다. 이름을 지정해주지 않으면 Entity의 이름을 변환한(Hospital이면 hospital이라는 이름을 가진 테이블) 테이블과 자동으로 연결한다
Physical Naming Strategy
): 모든 dot( . )는 언더바( _ )로 변환, Camel Case는 Snake Case로 변환, 모든 테이블은 소문자로 구성: ex. HelloHospital → hello_hospital DB 이름에 대문자와 소문자를 구분하고 싶거나 변수 이름을 그대로 사용하고 싶을 때는? : 변수 이름을 그대로 column과 매칭 시키고 싶으면 PhysicalNamingStrategyStandardImpl
전략을 이용하면 된다.application.yml에
spring.jpa.hibernate.naming.physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
추가
(3) http에서 넘어와 바인딩 된 값 또는 DB에서 넘어온 값을 entity인 Hospital로 변환할 수 있게 하는 HospitalDto
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class HospitalDto {
private int id;
private String managementNumber;
private int businessStatus; // 변경가능
private String phone;
private String roadNameAddress;
private String hospitalName;
private String businessTypeName;
private int healthcareProviderCount; // 변경가능
private int patientRoomCount; // 변경가능
private int totalNumberOfBeds; // 변경가능
private float totalAreaSize; // 변경가능
public HospitalDto(int businessStatus, int healthcareProviderCount, int patientRoomCount, int totalNumberOfBeds) {
this.businessStatus = businessStatus;
this.healthcareProviderCount = healthcareProviderCount;
this.patientRoomCount = patientRoomCount;
this.totalNumberOfBeds = totalNumberOfBeds;
}
public Hospital toEntity(){
return new Hospital(id, roadNameAddress, hospitalName, businessTypeName, healthcareProviderCount, patientRoomCount, totalNumberOfBeds);
}
}
(4) 전국 병원 데이터를 게시판의 형태로 볼 수 있게 해주는 list.mustache
{{>layouts/header}}
<table class="table">
<thead>
<tr>
<!-- id, 병원 이름, 병원 종류, 도로명 주소, 의료인 수, 입원실 수, 병상수를 조회할 수 있게 함-->
<th scope="col">id</th>
<th scope="col">hospital name</th>
<th scope="col">business type</th>
<th scope="col">address</th>
<th scope="col">number of healthcare provider</th>
<th scope="col">number of patient room</th>
<th scope="col">number of beds</th>
</tr>
</thead>
<tbody>
<!--model.addAttribute로 넘겨준 hospitals에서 정보를 꺼내어서 보여줌-->
<!--리스트 또는 배열의 형태는 반복문처럼 보여진다.-->
{{#hospitals}}
<tr>
{{#id}}
<th scope="row">{{id}}</th>
{{/id}}
{{^id}}
<th scope="row">unknown</th>
{{/id}}
<!-- 값에 null이 있는 결측치도 모두 보여줄 수 있게 함 -->
{{#hospitalName}}
<td>{{hospitalName}}</td>
{{/hospitalName}}
{{^hospitalName}}
<th scope="row">unknown</th>
{{/hospitalName}}
{{#businessTypeName}}
<td >{{businessTypeName}}</td>
{{/businessTypeName}}
{{^businessTypeName}}
<th scope="row">unknown</th>
{{/businessTypeName}}
...
</tr>
{{/hospitals}}
</table>
{{>layouts/footer}}
JpaRepository의 부모 인터페이스인 PagingAndSortingRepository에서 페이징 기능을 제공한다
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
...
/**
* Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}.
* @return a page of entities
*/
Page<T> findAll(Pageable pageable);
PagingAndSortingRepository의 findAll()을 보면 Pageable을 파라미터로 받으면 그 결과가 Page의 형태로 반환됨을 확인할 수 있다.
Pageable과 Page는 페이징을 구현할 때 필요한 메소드를 추상화 시켜놓은 인터페이스
페이징 옵션 설정하는 방법
PageRequest의 of 메소드 사용하기
PageRequest: Pageable의 구현체로 of() 메소드를 이용해서 페이지 번호, 페이지당 row 개수, 정렬(정렬 여부, 정렬 기준(속성)) 등을 설정할 수 있다
of 메소드 | 예시 | 코드 설명 |
---|---|---|
of(int page, int size) | findAll(PageRequest.of(10,3)) | 한 페이지에 3개의 row가 들어가도록 했을 때 10번(11번째) 페이지 |
of(int page, int size, Sort sort) | findAll(PageRequest.of(5,10,Sort.by(”age”).ascending())) | 한 페이지에 10개의 row가 들어가도록, age를 기준으로 오름차순으로 정렬했을 때 5번(6번째 페이지) |
of(int page, int size, Direction direction, String… properties) | findByUserNameContaining(”김”, PageRequest.of(5,20,DESC, “height”)) | 한 페이지에 20개의 row가 들어가도록, height를 기준으로 오름차순으로 정렬했을 때 5번(6번째 페이지) |
Direction: ASC 또는 DESC
@PageableDefault: 이 어노테이션을 이용해서도 페이징 옵션을 설정할 수 있다
- 정렬 조건을 여러개 넣고 싶은 경우 → @SortDefault 사용하기
- @SoreDefault에는 direction과 기준 column을 설정해줄 수 있다.
```java
@GetMapping("/list")
public String hospitalList(Model model, @PageableDefault(page=1, size=20)
@SortDefault.SortDefaults({@SortDefault(sort="healthcare_provider_count",
direction = Sort.Direction.DESC),
@SortDefault(sort = "patient_room_count",
direction = Sort.Direction.DESC)}) Pageable pageable){
Page<Hospital> hospitalList = hospitalRepository.findAll(pageable);
model.addAttribute("hospitals", hospitalList);
model.addAttribute("previous", pageable.previousOrFirst().getPageNumber());
model.addAttribute("next", pageable.next().getPageNumber());
return "list";
}
```
- 페이징 정보가 둘 이상일 때: @Qualifier를 사용
```java
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...)
```
파라미터 | 설명 |
---|---|
page | 보여주고자 하는 페이지(현재 페이지) |
size | 한 페이지에 들어갈 row의 수 |
value | size와 똑같이 한 페이지에 들어갈 row의 수를 설정할 수 있는 파라미터, 하지만 size를 사용하는 것을 권장한다 |
sort | 정렬조건(하나의 column에 대해서만 정렬 가능) |
(1) HospitalController의 HospitalList 메소드를 수정
: hospitalList와 findAll 파라미터에 Pageable을 추가한다.
이전과 이후 페이지로 이동하는 기능을 추가하기 위해 pageable의previousOrFirst().getPageNumber()와 next().getPageNumber()를 이용한다.
@GetMapping("/list")
public String hospitalList(Model model, Pageable pageable){
Page<Hospital> hospitalList = hospitalRepository.findAll(pageable);
// pageable을 받고 Page<>의 형태로 넣기
model.addAttribute("hospitals", hospitalList);
model.addAttribute("previous", pageable.previousOrFirst().getPageNumber());
model.addAttribute("next", pageable.next().getPageNumber());
return "list";
}
(2) list.mustache에 a tag를 이용해서 이전, 이후 페이지로 이동할 수 있는 버튼 생성
<ul class="pagination justify-content-center">
<li class="page-item">
<a class="page-link" href="?page={{previous}}">Previous</a>
<!-- model의 previous와 next에 이전 이후로 이동할 수 있는 기능을 넣어주었다. -->
</li>
<li class="page-item">
<a class="page-link" href="?page={{next}}">Next</a>
</li>
</ul>
HospitalRepository의 findByHospitalNameContaining()이라는 쿼리 메소드를 이용해서 keyword로 넘어온 값을 병원 이름과 비교하여 일치하는 결과 값을 List 형식으로 반환할 수 있다.
public interface HospitalRepository extends JpaRepository<Hospital, Integer> {
List<Hospital> findByHospitalNameContaining(String keyword);
// findBy: 조회하라
// HospitalName: Hospital의 hospitalName이라는 필드를 대상으로
// Containg: keyword를 포함하고 있는 모든 결과를
}
findBy로 시작 | 쿼리를 요청하는 메서드 임을 알림 |
---|---|
countBy로 시작 | 쿼리 결과 레코드 수를 요청하는 메서드 임을 알림 |
메서드 이름 키워드 | 샘플 | 설명 |
---|---|---|
And | findByEmailAndUserId(String email, String userId) | 여러필드를 and 로 검색 |
Or | findByEmailOrUserId(String email, String userId) | 여러필드를 or 로 검색 |
Between | findByCreatedAtBetween(Date fromDate, Date toDate) | 필드의 두 값 사이에 있는 항목 검색 |
LessThan | findByAgeGraterThanEqual(int age) | 작은 항목 검색 |
GreaterThanEqual | findByAgeGraterThanEqual(int age) | 크거나 같은 항목 검색 |
Like | findByNameLike(String name) | like 검색 |
IsNull | findByJobIsNull() | null 인 항목 검색 |
In | findByJob(String … jobs) | 여러 값중에 하나인 항목 검색 |
OrderBy | findByEmailOrderByNameAsc(String email) | 검색 결과를 정렬하여 전달 |
Containing | indByHospitalNameContaining(String keyword) | keyword가 포함된 모든 결과를 전달 |
(1) HospitalController에 search 메소드 추가
@GetMapping("/search")
public String search(String keyword, Model model){
List<Hospital> searchList = hospitalRepository.findByHospitalNameContaining(keyword);
model.addAttribute("searchList", searchList);
return "search";
}
(2) header에 input 부분에 정보를 입력하면 그게 keyword로 넘어감
→ searchList의 key로 들어가서 검색된 정보가 model의 searchList로 넘어감
<form class="d-flex" role="search" action="/hospital/search" method="GET">
<div class="btn-group" role="group" aria-label="Basic example">
<input class="form-control me-2" name="keyword" type="text" placeholder="병원 이름을 입력해주세요">
<button class="btn btn-outline-success" type="submit">검색</button>
</div>
</form>
(3) model의 searchList를 보여주는 search 페이지 구현
{{>layouts/header}}
검색 결과입니다.
<table class="table">
<thead>
<tr>
<th scope="col">id</th>
<th scope="col">hospital name</th>
<th scope="col">business type</th>
<th scope="col">address</th>
<th scope="col">number of healthcare provider</th>
<th scope="col">number of patient room</th>
<th scope="col">number of beds</th>
</tr>
</thead>
<tbody>
{{#searchList}}
<tr>
{{#id}}
<th scope="row">{{id}}</th>
{{/id}}
{{^id}}
<th scope="row">unknown</th>
{{/id}}
{{#hospitalName}}
<td>{{hospitalName}}</td>
{{/hospitalName}}
{{^hospitalName}}
<th scope="row">unknown</th>
{{/hospitalName}}
...
</tr>
{{/searchList}}
</table>
{{>layouts/footer}}