지난 번까지는 각 테이블 간의 연관관계를 하나도 지정해주지 않고 기본적인 작동 방식만 알아보았다. 이번에는 PK-FK로 연관된 테이블의 관계를 엔티티에서 역시 맺어보고자 한다.

이 프로젝트의 테이블들의 상관관계는 다음과 같다.

상품과 관련된 prod 테이블이 있고, 상품을 조회하면 글/이미지 쌍으로 디테일이 있는 prod_detail 테이블이 있다. 두 테이블은 1(prod) : N(prod_detail) 관계이다. prod 테이블의 no 컬럼이 PK이며, prod_detail의 prod_no 컬럼이 prod 테이블을 참조하는 컬럼이다. 상품이 삭제되면 디테일도 삭제되어야 하기 때문에 양방향 관계를 가진다.

이 방향이라는 것은, 양방향/ 단방향이 있다.

테이블 개념에서 볼 때 PK-FK로 연관관계를 맺고 있으면 방향이랄 것이 없다.

하지만 JPA 환경에서 엔티티들은 기본적으로 단방향이다. Prod 엔티티에 @OneToMany 어노테이션으로 ProdDetail을 명시해주면 Prod->ProdDetail의 방향이 된다. 테이블의 양방향 상태처럼 만들어주려면 ProdDetail 엔티티에서도 @ManyToOne 설정을 해 주어야 한다.

 

@Getter
@Setter
@ToString
@Table(name = "prod")
@Entity
public class Prod {

	@Id
	@GeneratedValue
	@Column(name = "no")
	private Long no;
	private String name;
	private String thumbnailUrl;
	private Long originPrice;
	private Long discPrice;
	private String description;
	private LocalDateTime createdAt;
	@Transient
	private boolean inBasket;

	@PrePersist
	public void createdAt() {
		this.createdAt = LocalDateTime.now();
	}

	@OneToMany(mappedBy = "prod", fetch = FetchType.EAGER)
	private List<ProdDetail> detailList = new ArrayList<ProdDetail>();
}
@Getter
@Setter
@ToString
@Table(name = "prod_detail")
@Entity
public class ProdDetail {

	@Id
	private Long id;
//	@Column(name = "prod_no")
//	private Long prodNo;
	private String content;
	private String imageUrl;
	private LocalDateTime createdAt;

	@PrePersist
	public void createdAt() {
		this.createdAt = LocalDateTime.now();
	}

	@ManyToOne
	@JoinColumn(name = "prod_no", nullable = false, updatable = false)
	@JsonIgnore
	private Prod prod;

	public void setProd(Prod prod) {
		if (this.prod != null) {
			this.prod.getDetailList().add(this);
		}
		this.prod = prod;
		prod.getDetailList().add(this);
	}

}

@OneToMany의 mappedBy 속성은 양방향 매핑할 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 준다. 이렇게 되면 서로 참조할 수 있는 구조를 가진다.

1:N에서 N입장인 ProdDetail에서 @JoinColumn을 명시해주어야 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다. 반드시 name 속성에 참조하는 테이블(prod)_기본키(no) 형태로 적어주어야 한다.

이렇게 하면 양방향 관계 설정이 된다.

 

상품을 조회할 때 상품과 디테일의 글/이미지 모두를 조회하는 API를 만들어보고자 한다.

@RestController
@RequestMapping("/prod")
public class ProdController {

	private static final Logger logger = LoggerFactory.getLogger(ProdController.class);

	@Autowired
	ProdService prodService;

	@GetMapping("/{id}")
	public ResponseVO findProdWithDetailByUser(@CookieValue(value = "accesstoken", required = false) String accesstoken,
			@PathVariable("id") Long no) throws Exception {
		logger.info("call findProdWithDetailByUser()");
		
		Optional<Prod> prodResult = prodService.findProdWithDetailByUser(accesstoken, no);
		ResponseVO result = new ResponseVO();
		result.setCode(HttpStatus.OK);
		result.setMessage("SUCCESS");
		result.setData(prodResult);
		return result;
	}	
}
@Service
public class ProdService {

	private static final Logger logger = LoggerFactory.getLogger(ProdService.class);

	@Autowired
	TokenRepository tokenRepo;

	@Autowired
	ProdRepository prodRepo;

	@Autowired
	BasketRepository basketRepo;

	public Optional<Prod> findProdWithDetailByUser(String accesstoken, Long no) {
		logger.info("call findProdWithDetailByUser()");

		Optional<Prod> prodResult = prodRepo.findById(no);

		Token token = tokenRepo.findByToken(accesstoken);
		String email = token.getUserEmail();
		Basket basket = basketRepo.findByUserEmailAndProdNo(email, no);
		if (basket != null) {
			boolean inBasket = true;
			prodResult.get().setInBasket(inBasket);
			return prodResult;
		} else {
			return prodResult;
		}
	}

}

(로그인했을 시 장바구니에 담겨 있는지 없는지 여부를 확인하는 로직도 있다.)

findById 메소드를 사용할 것이기 때문에 레포지토리에 따로 메소드를 생성할 필요는 없다.

 

포스트맨을 사용해서 1번 상품을 조회한다.

성공적으로 상품을 조회한다.

콘솔을 확인하면, findById 메소드가 실행되었을 때 left outer join으로 prod_detail의 데이터까지 조회한 것을 확인할 수 있다.

 

@OneToMany, @ManyToOne 등의 설정은 JPA로 테이블 간의 관계 설정을 보다 손쉽게 해 주는  또 하나의 특징적인 기능이라는 것을 이번 프로젝트 때 알게 되었다.

참조한 블로그

목차

1. DAO, Service, Controller 작성

2. REST API를 사용하여 회원가입 기능 구현

이번에는 간단히 CRUD 기능을 사용하기 위해 DAO, Service, Controller를 작성하는 것에 대해 알아보고자 한다.

 

1. DAO, Service, Controller 작성

MyBatis 환경에서 DAO와 동일한 개념이 레포지토리 인터페이스이다. 여기서 JPA의 특징적인 점은 별도의 구현 클래스를 만들지 않고 인터페이스만 정의함으로써 기능을 사용할 수 있다는 것이다.

여기서 보통 Spring Data 모듈에서 제공하는 CrudRepository를 상속한다. 추가적으로 페이징 처리를 할 경우에는 PagingAndSortingRepository를 사용한다.

package com.codepresso.persistence;

import org.springframework.data.repository.CrudRepository;

import com.codepresso.domain.User;

public interface UserRepository extends CrudRepository<User, Long> {

}

 

이렇게 간단한 인터페이스만으로도 데이터 입력이 잘 되는지 보기 위해 간단히 컨트롤러와 서비스를 작성한다.

@RestController
@RequestMapping("/user")
public class UserController {
	
	@Autowired
	private UserService userService;
	
	@PostMapping("/signup")
	public User signUp(@RequestBody User user) {
		User userResult = userService.signUp(user);
		return userResult;
	}

}
@Service
public class UserService {

	@Autowired
	UserRepository userRepo;

	public User signUp(User user) {
		User userResult = userRepo.save(user);
		return userResult;
	}

}

userRepo에 save라는 메소드를 생성하지 않았지만 CrudRepository를 상속하였기 때문에 기본적인 CRUD의 메소드를 바로 사용할 수 있다. ㄹㅇ 편리 ㅎㅎ

CrudRepository가 기본적으로 제공하는 메소드는 다음과 같다.

Modifier and Type Method and Description
long count()

Returns the number of entities available.

void delete(T entity)

Deletes a given entity.

void deleteAll()

Deletes all entities managed by the repository.

void deleteAll(Iterable<? extends T> entities)

Deletes the given entities.

void deleteById(ID id)

Deletes the entity with the given id.

boolean existsById(ID id)

Returns whether an entity with the given id exists.

Iterable<T> findAll()

Returns all instances of the type.

Iterable<T> findAllById(Iterable<ID> ids)

Returns all instances of the type T with the given IDs.

Optional<T> findById(ID id)

Retrieves an entity by its id.

<S extends T>
S
save(S entity)

Saves a given entity.

<S extends T>
Iterable<S>
saveAll(Iterable<S> entities)

Saves all given entities.

그리고 createdAt 변수를 지난번처럼 정의하였더니 DB에 올라가질 않아서 엔티티가 되는 User 클래스는 다음과 같이 수정하였다. 참조

@Getter
@Setter
@ToString
@Entity
public class User {

	@Id
	private String email;
	private String name;
	private String birth;
	private String password;
	@Transient
	private String passwordCheck;
	private String gender;
	private LocalDateTime createdAt;

	@PrePersist
	public void createdAt() {
		this.createdAt = LocalDateTime.now();
	}

}

프로젝트를 실행하고 다음과 같이 포스트맨에서 RequestBody에 필요한 부분들을 입력해주었다.

정상 작동됨을 확인하였다.

 

2. REST API를 사용하여 회원가입 기능 구현

이메일 중복 체크와 비밀번호 확인을 할 수 있는 회원가입 기능을 구현하고자 한다.

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
	private UserService userService;
	
	@PostMapping("/signup")
	public ResponseVO signUp(@RequestBody User user) throws Exception {
		
		int emailResult = userService.checkEmail(user);
		boolean pwResult = userService.checkPw(user);
		
		if (emailResult == 0 && pwResult == true) {
			User userResult = userService.signUp(user);
			ResponseVO result = new ResponseVO();
			result.setCode(HttpStatus.OK);
			result.setMessage("SUCCESS");
			result.setData(userResult);
			return result;
		} else {
			ResponseVO result = new ResponseVO();
			result.setCode(HttpStatus.INTERNAL_SERVER_ERROR);
			result.setMessage("FAIL");
			result.setData(null);
			return result;
		}
	}
    
}
@Service
public class UserService {

	@Autowired
	UserRepository userRepo;
	
	@Autowired
	TokenRepository tokenRepo;

	public int checkEmail(User user) {		
		String email = user.getEmail();
		int emailResult = userRepo.countByEmail(email);
		return emailResult;
	}
	
	public boolean checkPw(User user) {		
		logger.info("user: " + user);
		String password = user.getPassword();
		String passwordCheck = user.getPasswordCheck();
		
		if (password.equals(passwordCheck)) {
			return true;
		} else
			return false;
	}
	
	public User signUp(User user) {
		User userResult = userRepo.save(user);
		return userResult;
	}
    
}
@Repository
public interface UserRepository extends CrudRepository<User, String> {
	
	public int countByEmail(String email);
    
}

눈여겨 볼 메소드는

public int countByEmail(String email);

이다.

MyBatis를 사용했으면 매퍼 파일에

<select id="checkEmail" resultType="int">
	<![CDATA[
		SELECT COUNT(*) FROM user
		WHERE email = #{email};
	]]>
</select>

다음과 같이 적었어야 하지만 UserRepository에 count메소드만 선언해주면 위와 같은 쿼리를 실행한다.

보다 복잡한 쿼리는 어떻게 다루어야 하는지는 더 공부가 필요할 듯 하다.

목차

1. perspective 설정

2. 새 프로젝트 생성

3. application.properties 작성

4. 엔티티 클래스 매핑

 

* Git Repository에 업로드

기존에 MyBatis로 DB를 사용했던 것을 JPA로 전환하려고 한다.

작업한 코드는 여기에 업로드하였다.

 

1. perspective 설정

Spring 선택

 

2. 새 프로젝트 생성

Spring Starter Project 선택

정보 입력

5가지 의존성 추가

 

3. application.properties 작성

# DataSource Setting
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/discountak?characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=사용자이름
spring.datasource.password=비밀번호

# JPA Setting
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=false
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.format_sql=true

# Logging Setting
logging.level.org.hibernate=info

참고로 본 프로젝트는 이 책을 참조하였다.

책은 H2를 사용했지만 나는 MySQL을 사용하니 DB 드라이버만 다르게 하고 거의 비슷하게 설정하였다.

여기서, 

spring.jpa.hibernate.ddl-auto=update

update로 설정해야 하는 이유는, 이미 저장해놓은 데이터들이 있는 스키마를 사용하기 때문에 create로 작성하면 테이블이 중복되는 오류가 발생한다.

스프링부트에서 JPA를 사용하기 위한 보다 자세한 설명은 여기에서 볼 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest
class DiscounbootApplicationTests {

	@Test
	void contextLoads() {
	}

}

src/test/java의 기본 테스트 클래스에 다음과 같이 작성한 후 실행하면 MySQL과 정상적으로 연결됨을 확인할 수 있다.

4. 엔티티 클래스 매핑

JPA는 DB의 테이블이 아닌 엔티티를 통해 데이터를 관리하기 때문에 테이블과 매핑할 엔티티 클래스를 작성해야 한다.

기존 MyBatis를 사용할 때 domain 패키지의 VO 클래스와 비슷한 개념이다.

@Getter
@Setter
@ToString
@Entity
public class User {

	@Id @GeneratedValue
	private String email;
	private String name;
	private String birth;
	private String password;
	@Transient
	private String passwordCheck;
	private String gender;
	@Temporal(value = TemporalType.TIMESTAMP)
	private Date createdAt;

}

@Entity는 User 클래스를 엔티티 처리한다.

@Id, @GenerateValue는 변수 email을 식별자로 처리한다.

@Transient는 특정 변수를 영속 필드에서 제외할 때 사용한다. 테이블에 존재하지 않는 변수이기 때문에 @Transient 처리하였다.

 

자바 애플리케이션으로 프로젝트를 실행하면 정상 작동한다.

 

더보기

에러

user 외의 다른 엔티티 클래스도 작성하고 JUnit 테스트를 실행했더니 갑자기 뜨는 

'no test found with test runner 'junit 5' 에러...

이럴 때에는 침착하게 이클립스를 종료하고 재실행하면 정상 작동된다.

이런 에러 나만 뜨나 ㅋ


지금까지의 작업을 MyBatis와 비교해보고자 한다. MyBatis로 작업을 했다면, 해야 하는 설정이 한두 가지가 아니다.

VO클래스는 공통적으로 작성하였지만, MyBatis 환경에서는 추가적인 작업이 더 필요하다.

먼저 VO의 알리아스를 설정해야 하며,

매퍼파일에 일일이 쿼리를 작성해주어야 하는 번거로움이 있다. 

반면, 프로젝트를 생성할 때 추가해주었던 JPA 의존성으로 인해 생성된 JPA 스타터는 JPA 연동에 필요한 라이브러리들과 복잡한 xml 설정을 자동으로 처리하는 효율성을 가지고 있다. 

 

다음 글에서는 DAO 단을 JPA환경에서 어떻게 작성하는지 알아보고자 한다.

 

* 번외: Git Repository 네이밍

작업한 것들을 Git에 하나씩 올리려다가 문득 레포지토리 네이밍에 대해 생각해보게 되었다.

1. 소문자 사용

2. _(밑줄)이 아닌 -(줄표) 사용

3. 구체적으로 명시하기

4. 일관되게 작성하기

참조한 사이트

전체 텍스트 검색은 긴 문자의 텍스트 데이터를 빠르게 검색하기 위한 MySQL의 부가적인 기능이다. 전체 텍스트 검색을 사용하면 기사의 내용에 포함된 여러 단어들에 인덱스가 설정되어서 검색 시에 인덱스를 사용하여 검색 속도가 월등히 빨라진다.

전체 텍스트 인덱스는 신문기사와 같이 텍스트로 이루어진 문자열 데이터의 내용을 가지고 생성한 인덱스를 말한다.

SELECT * FROM 신문기사_테이블 WHERE 신문기사내용 '교통 사고의 증가로 인해 오늘 ----';
SELECT * FROM 신문기사_테이블 WHERE 신문기사내용 LIKE '교통%';

인덱스가 정렬되어 있으므로, 해당되는 내용이 인덱스를 통해서 빠르게 검색함.

SELECT * FROM 신문기사_테이블 WHERE 신문기사내용 LIKE '%교통%';

 

전체 테이블 검색을 하게 됨. 만약 10년치 기사에서 검색한다면 과부하가 발생하고 응답시간도 긺.

이것을 전체 텍스트 검색이 해결한다. 전체 텍스트 검색은 첫 글자 뿐 아니라 중간의 단어나 문장으로도 인덱스를 생성해 주기 때문에 이와 같은 상황에서도 인덱스(정확히는 전체 텍스트 인덱스)를 사용할 수 있어 순식간에 검색 결과를 얻을 수 있다.

전체 텍스트 인덱스는 신문기사와 같이, 텍스트로 이루어진 문자열 데이터의 내용을 가지고 생성한 인덱스를 말한다. MySQL에서 생성한 일반적인 인덱스와는 몇 가지 차이점이 있다.

전체 텍스트 인덱스는 InnoDB MylSAM 테이블만 지원한다.

- 전체 텍스트 인덱스는 char, varchar, text의 열에만 생성이 가능하다.

- 인덱스 힌트의 사용이 일부 제한된다.

- 여러 개의 열에 FULLTEXT 인덱스를 지정할 수 있다.

 

1. 전체 텍스트 인덱스 생성

3가지 방법:

CREATE TABLE 테이블이름(

             …

             열이름 데이터형식,

             …,

             FULLTEXT 인덱스이름 (열이름)

);
CREATE TABLE 테이블이름(

             …

             열이름 데이터형식,

             …,

);

ALTER TABLE 테이블이름

                 ADD FULLTEXT (열이름);
CREATE TABLE 테이블이름(

             …

             열이름 데이터형식,

             …,

);

CREATE FULLTEXT INDEX 인덱스이름

                 ON 테이블이름 (열이름);

 

2. 전체 텍스트 인덱스 삭제

ALTER TABLE 테이블이름
	DROP INDEX FULLTEXT (열이름);

3. 중지 단어(stopwords)

실제로 검색할 때 무시할 만한 단어들은 아예 전체 텍스트 인덱스로 생성하지 않는 편이 좋다.

이번

선거는

아주

중요한

행사이므로

모두

참여

바랍니다

이번’, ‘아주’, ‘모두’, ‘등과 같은 단어는 검색할 이유가 없으므로 제외한다. 이것이 중지 단어이다.

MySQL 5.7INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD 테이블에 약 36개의 중지 단어를 가지고 있다.

 

4. 전체 텍스트 검색을 위한 쿼리

전체 텍스트 인덱스를 생성한 후에 이를 사용하기 위한 쿼리는 일반 SELECT문의 WHERE절에 MATCH() AGAINST() 사용하면 된다.

MATCH (col1, col2, ...) AGAINST (expr [search_modifier])

search_modifier:
  {
       IN NATURAL LANGUAGE MODE
     | IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
     | IN BOOLEAN MODE
     | WITH QUERY EXPANSION
  }

- 자연어 검색

특별히 옵션을 지정하지 않거나 IN NATURAL LANGUAGE MODE를 붙이면 자연어 검색을 한다. 자연어 검색은 단어가 정확한 것을 검색해 준다.

newspaper이라는 테이블의 article이라는 열에 전체 텍스트 인덱스가 생성되어 있다고 가정한다.

영화라는 단어가 들어간 기사를 찾으려면 다음과 같이 사용한다.

SELECT * FROM newspaper 
	WHERE MATCH(article) AGAINST('영화');

영화는’, ‘영화가등 검색 불가.

영화또는 배우두 단어 중 하나가 포함된 기사 검색.

SELECT * FROM newspaper 
	WHERE MATCH(article) AGAINST('영화 배우');

- BOOLEAN MODE 검색

단어나 문장이 정확히 일치하지 않는 것도 검색하는 것을 의미한다.

+

검색 필수

SELECT * FROM newspaper

                 WHERE MATCH(article) AGAINST('영화 +액션' IN BOOLEAN MODE);

-

검색 제외

SELECT * FROM newspaper

                 WHERE MATCH(article) AGAINST('영화 -액션' IN BOOLEAN MODE);

~

검색 부정(-보다 부드러운 방식)

SELECT * FROM newspaper

                 WHERE MATCH(article) AGAINST('영화 ~액션' IN BOOLEAN MODE);

영화를 찾되 액션이 없는 열보다 액션이 있는 열이 아래 순위

*

부분 검색

SELECT * FROM newspaper

                 WHERE MATCH(article) AGAINST('영화*' IN BOOLEAN MODE);

영화를’, ‘영화가’, ‘영화는

“”안에 있는 구문과 정확히 동일한 철자의 구문

부분 검색

SELECT * FROM newspaper

                 WHERE MATCH(article) AGAINST("재밌는 영화" IN BOOLEAN MODE);

재밌는 영화”, “재밌는 영화가

재밌는 한국 영화”, “재밌는 할리우드 영화불가

5. 실습

MySQL은 기본적으로 3글자 이상만 전체 텍스트 인덱스로 생성한다. 이러한 설정을 2글자까지 전체 텍스트 인덱스가 생성되도록 시스템 변숫값을 변경한다.

아직 인덱스를 만들지 않은 상태.

실행계획(전체 테이블 검색)

전체 텍스트 인덱스 생성.

실행계획(전체 텍스트 인덱스 검색)

남자또는 여자가 들어간 행 검색.

남자’, ‘여자모두 들어간 행 검색.

남자가 들어간 행 중에 여자는 제외된 행 검색.

전체 텍스트 인덱스로 만들어진 단어들.

원래 있던 전체 텍스트 인덱스를 삭제하고 불필요한 중지 단어를 지정.

 

'MySQL' 카테고리의 다른 글

MySQL 잠금기능  (0) 2020.02.20
MySQL view(뷰)  (0) 2020.02.17
MySQL SELECT문  (1) 2020.02.10

잠금(Locking)을 알기 위해선 그 전에 동시성 제어(Concurrency Control)를 먼저 짚고 넘어가야 한다. 동시성 제어는 다중 사용자 환경을 지원하는 데이터베이스 시스템에서 동시에 실행되는 여러 트랜잭션 간의 간섭으로 문제가 발생하지 않도록 트랜잭션의 실행 순서를 제어하는 기법이다. 한 트랜잭션을 실행하는 중에 다른 트랜잭션이 간섭하게 되면 갱신 분실(lost update), 연쇄 복귀(cascading rollback) 또는 회복 불가능(unrecoverability), 불일치 분석(inconsistent analysis) 등과 같은 문제들이 발생할 수 있다.

동시성 제어는 다시 비관적 동시성 제어 (Pessimistic concurrency Control)와 낙관적 동시성 제어(Optimistic concurrency control)로 나뉠 수 있다. 비관적 동시성 제어란 사용자들이 같은 데이터를 동시에 수정할 것이라고 가정하여 데이터를 읽는 시점에 Locking을 걸고 트랜잭션이 완료될 때까지 이를 유지하는 것을 말한다. 반면, 낙관적 동시성 제어란 사용자들이 같은 데이터를 동시에 수정하지 않을 것이라고 가정하여 데이터를 읽을 때 Locking을 설정하지는 않는다.

한 트랜잭션의 실행 중에 다른 트랜잭션의 끼어들기로 발생한 문제를 해결하기 위해서는 동시성(병행 수행)을 최대한 보장하면서 직렬 스케줄과 동일한 결과를 얻을 수 있는 직렬 가능한 스케줄로 만들도록 해야 한다. Locking은 트랜잭션의 실행 순서를 강제로 제어하여 직렬 가능한 스케줄이 되도록 보장하는 방법이다.

Locking은 하나의 트랜잭션이 실행하는 동안 특정 데이터 항목에 대해서 다른 트랜잭션이 동시에 접근하지 못하도록 상호배제(Mutual Exclusive)하는 기법이다. 하나의 트랜잭션이 데이터 항목에 대하여 잠금(lock)을 설정하면, 잠금을 설정한 트랜잭션이 해제(unlock)할 때까지 데이터를 독점적으로 사용할 수 있다.

Locking은 기본적으로 lock 연산과 unlock 연산을 사용한다.

Locking의 연산은 데이터에 대한 연산의 성격에 따라 공유잠금(Shared lock: S-lock)과 배타잠금(Exclusive lock: X-lock)으로 나눌 수 있다. 모든 트랜잭션은 데이터 항목에 대한 접근을 시도할 때 둘 중 하나의 잠금 방법을 사용한다. S-lock X-lock 모두 잠금을 해제할 경우에는 unlock 연산을 이용한다.

S-lock을 설정한 트랜잭션은 데이터에 대해 읽기 연산만 가능하다. 한 데이터에 대해 T1S-lock을 설정했다면 T2역시 S-lock이 실행되는 동안에는 읽기 연산만이 가능하다. 또한 하나의 데이터 항목에 대해 여러 개의 S-lock이 가능하다. 예를 들어 T1에서 x에 대해 S-lock을 설정한 경우, 동시에 T2에서도 x에 대해 S-lock을 설정할 수 있다.

트랜잭션이 X-lock을 설정하면 데이터 항목에 대해서 읽기 연산과 쓰기 연산 모두 가능하다. 또한 하나의 데이터 항목에 대해서는 하나의 X-lock만 가능하다. 동시에 여러 개의 X-lock은 불가능한데, 예를 들어 T1이 한 데이터에 대해 X-loc을 설정하였다면 unlock하기 전까지는 T2가 그 데이터에 X-lock을 설정할 수 없다.

S-lockX-lock에 대한 표를 아래에서 확인할 수 있다.

Lock에 대해서는 다음과 같은 규칙을 지켜야 한다.

-        트랜잭션은 데이터 항목 x에 대해 read(x) 연산을 실행하기 전에 S-lock(x)이나 X-lock(x) 중 하나를 실행해야 한다.

-        write(x) 연산을 실행하기 위해서는 X-lock(x)을 실행해야 한다.

-        연산 종료 후에는 unlock(x) 연산을 실행해야 한다.

-        S-lock(x)이나 X-lock(x) 연산 실행 후에만 unlock(x) 연산을 실행할 수 있다.

Locking의 대상이 되는 데이터 객체의 크기를 단위로 나타낼 수 있다. 작게는 레코드의 필드 값, 하나의 레코드, 물리적 입출력 단위가 되는 디스크 블록이 될 수도 있으며, 크게는 테이블이나 데이터베이스까지 하나의 잠금 단위가 될 수 있다. 잠금 단위가 클수록 동시성(병행성) 수준은 낮아지고, 동시성 제어 기법은 간단해진다. 반면, 잠금 단위가 작을수록 동시성(병행성) 수준은 높아지고, 관리는 복잡해진다(: 잠금단위가 레코드인 경우vs테이블인 경우.)

Locking은 트랜잭션 작업의 완전성을 보장해주는 기능처럼 보이지만 그 한계도 존재한다. 첫째로 교착상태(deadlock)가 발생할 수 있으며, 둘째로 직렬 가능한 스케줄이 항상 보장되지만은 않는다는 것이다.

위와 같이 두 트랜잭션이 교착상태에 빠지면 외부에서 강제로 트랜잭션을 중단하거나 잠금을 해제하지 않는 이상 무한정 대기 상태로 남게 된다.

위에서 직렬 가능한 스케줄이 항상 보장되지만은 않는다고 하였는데 이를 보완하는 것이 2단계 잠금 규약(2-Phase Locking protocol: 2PL)이다. 2PL은 잠금을 설정하는 단계와 해제하는 단계로 나누어 수행하도록 하는 것이다. 먼저 트랜잭션이 lock 연산만 수행할 수 있고 unlock 연산은 수행할 수 없는 확장단계(growing phase), 다음으로 트랜잭션이 unlock 연산만 수행할 수 있고 lock 연산은 수행할 수 없는 축소단계(shrinking phase)를 거친다.

왼쪽, 오른쪽 모두 2PL을 준수한 사례인데 오른쪽은 그럼에도 불구하고 교착상태가 발생했다. 이러한 문제를 해결하기 위해서는 각 트랜잭션을 시작하기 전에 모든 필요한 잠금을 동시에 설정할 수 있다. 또는 교착상태 회피 방법이나 탐지 방법을 통해 교착상태를 해결해야 한다.

 

http://www.gurubee.net/lecture/2398

https://medium.com/pocs/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-%EA%B8%B0%EB%B2%95-%EC%9E%A0%EA%B8%88-locking-%EA%B8%B0%EB%B2%95-319bd0e6a68a

'MySQL' 카테고리의 다른 글

MySQL 전체 텍스트 검색  (0) 2020.03.19
MySQL view(뷰)  (0) 2020.02.17
MySQL SELECT문  (1) 2020.02.10

뷰의 작동 방식은 다음과 같다. 뷰를 생성하여 SELECT문으로 데이터를 조회하면 테이블과 동일한 모양으로 확인할 수 있다. 

 

뷰를 사용하는 이유는 다음과 같은 장점이 있기 때문이다.

보안에 도움이 된다: 

위와 같은 뷰에는 사용자의 이름과 주소만 있을 뿐, 시용자의 중요한 개인정보인 출생년도, 연락처, 키, 가입일 등은 들어 있지 않다. 예를 들어 다른 사용자에게 무언가 작업을 맡겼을 때 연락처 등의 중요한 정보에는 접근하지 않고 이름과 주소 데이터만 가지고 작업하도록 할 때 뷰를 사용할 수 있다.

 

복잡한 쿼리를 단순화할 수 있다:

이렇게 줄도 길고 여러 조건이 들어간 쿼리를 자주 사용하게 될 경우 이 쿼리문을 뷰로 만들어 놓으면 편리하다.

 


  • CREATE VIEW Statement
  • ALTER VIEW Statement
  • DROP VIEW Statement
  • Using Views

1. View Syntax

2. View Processing Algorithms

3. Updatable and insertable Views

4. The View WITH CHECK OPTION Clause

5. View Metadata


  • CREATE VIEW Statement

CREATE VIEW 명령문은 새 뷰를 만든다. OR REPLACE 절을 뒤에 붙이면 존재하고 있던 뷰를 대체한다. 

 

여기서 select_statement는 만들고자 하는 뷰를 정의해주는 SELECT 명령문이다. select_statement는 기존의 테이블이나 다른 뷰들의 SELECT 명령문이 될 수 있다.

 

뷰는 만든 시간에 멈취있으며, 뷰를 만든 이후에는 뷰의 근본이 되는 테이블을 정의하는 변화에 영향을 받지 않는다. 예를 들어 어떤 테이블의 SELECT *을 사용한 뷰가 있다면, 새 컬럼들이 그 테이블에 나중에 추가되어도 그 뷰에는 추가되지 않는다. 뷰에서 그 컬럼들을 선택하면 에러가 발생한다.

 

ALGORITHM 절은 MySQL이 뷰를 처리하는 데 영향을 미친다. 

DEFINER와 SQL SECURITY 절은 뷰를 만들 때 접근권한을 체크할 때 사용한다. 

WITH CHECK OPTION 절은 뷰에 참조된 테이블에 행을 INSERT 또는 UPDATE 하는 것을 제약한다.

 

(CREATE VIEW 명령문에서는 다른 곳에서 SELECT 명령문이 쓰인 컬럼에 대한 SELECT 우선권을 가진다. OR REPLACE 절이 있으면 뷰에 대한 DROP 우선권도 가진다. )

 

뷰는 데이터베이스에 속한다. 뷰를 만들면 기본 설정되어 있는 데이터베이스에 생성되는 것이 디폴트이다. 다른 데이터베이스에 뷰를 만들고 싶으면 db_name.view_name 구문을 사용하여 원하는 데이터베이스의 이름을 명시한다.

 

한 데이터베이스 안에서는 베이스 테이블과 뷰가 같은 namespace를 공유하기 때문에 뷰 이름을 테이블 이름과 동일하게 설정할 수 없다.

 

뷰는 베이스 테이블과 마찬가지로 중복되지 않는 유니크한 컬럼명을 가진다. AS 뒤의 SELECT문에 사용된 컬럼명이 뷰의 컬럼 이름으로 사용되는 것이 디폴트이다. 베이스 테이블의 컬럼명과 다르게 컬럼명을 설정하려면  VIEW view_name 뒤에 [(column_list)]와 같이 컬럼들의 이름을 지정한다. column_list에는 반드시 SELECT 문의 컬럼 수와 동일한 컬럼을 작성해야 한다.

 

뷰를 만들 때 SELECT문을 사용하여 테이블이나 다른 뷰를 선택한다고 하기는 했지만 SELECT문 없이 아무 테이블도 지정하지 않을 수도 있다.

CREATE VIEW v_today (today) AS SELECT CURRENT_DATE;


  • ALTER VIEW Statement

이 명령문은 정의된 뷰를 수정하는 것인데 여기서 뷰는 반드시 존재하는 것이어야 한다. 문법은 CREATE VIEW와 거의 유사하다.


  • DROP VIEW Statement

(맞게 해석했는지 모르겠지만) DROP 명령문에 존재하지 않는 뷰의 이름을 넣으면 그 뷰를 DROP할 수 없을 뿐만 아니라 존재하고 있는 모든 뷰 역시도 DROP된다는 에러가 발생한다.

If any views named in the argument list do not exist, the statement returns an error indicating by name which nonexisting views it was unable to drop, but also drops all views in the list that do exist.


  • Using Views

1. View Syntax

CREATE VIEW

ALTER VIEW

DROP VIEW


2. View Processing Algorithms

MySQL의 CREATE VIEW나 ALTER VIEW 와 같은 ALGORITHM절은 표준 SQL에 대한 MySQL의 확장이다. ALGORITHM은 MERGE, TEMPTABLE, UNDEFINED 세 변수를 가진다.

 

MERGE는 뷰와 뷰 정의문을 참조하는 명령문의 문장은 명령문의 부분과 상응되는 뷰 대체 정의문 부분과 병합된다.

TEMPTABLE은 뷰에서 나온 결과는 임시 테이블 속으로 들어가게 되는데, 이것은 명령문을 실행하기 위해 사용된다.

UNDEFINED는 MySQL은 사용할 알고리즘을 선택한다. 가능하다면 MySQL은 MERGE를 선호하게 되는데, 왜냐하면 MERGE는 일반적으로 보다 효율적이며 또한 뷰는 임시 테이블이 사용되는 경우에는 업데이트를 할 수 없기 때문이다.

 

확실하게 TEMPTABLE가 선택되는 경우는 임시 테이블이 생성되고 임시 테이블이 명령문 수행을 마치기 위해 사용되기 전에 잠금이 언더라잉(underlying) 테이블에서 릴리즈될 수 있을 경우만 해당된다. 이 결과로 잠금 릴리즈가 MERGE알고리즘 보다 빨리 구현되며, 따라서 뷰를 사용하는 다른 클라이언트가 더 이상 갇히지 않게 된다.

 

뷰 알고리즘은 세 가지 이유로 UNDEFINED될 수 있다.

  • CREATE VIEW명령문안에 ALGORITHM구문이 전혀 없다.
  • CREATE VIEW명령문이 명확한 ALGORITHM = UNDEFINED구문을 가지고 있다.
  • ALGORITHM = MERGE은 임시 테이블이로 처리되는 뷰에 대해서만 지정된다. 이 경우, MySQL은 경고문을 내 보내고 알고리즘을 UNDEFINED으로 설정한다.

3. Updatable and insertable Views

기본적으로 뷰는 읽기 전용으로 많이 사용되지만 어떤 뷰들은 업데이트가 가능하다. UPDATEDELETE, INSERT와 같은 명령문에 있는 뷰들을 사용해서 언더라잉 테이블의 내용을 업데이트 할 수가 있다. 또한, 업데이트가 되지 않도록 뷰를 구성할 수도 있다. 보다 특징적으로 하기 위해서, 뷰는 아래의 것 중에 하나를 가지고 있으면 업데이트가 되지 않는다.

  • Aggregate functions (SUM(), MIN(), MAX(), COUNT(), 등등)
  • DISTINCT
  • GROUP BY
  • HAVING
  • UNION또는 UNION ALL
  • 선택한 리스트에 있는 서브 쿼리
  • 조인(Join)
  • FROM구문에 있는 업데이트할 수 없는(non-updatable) 뷰
  • FROM구문에 있는 테이블을 참조하는 WHERE구문에 있는 서브쿼리
  • 문자 값만을 참조 (이와 같은 경우, 업데이트할 언더라잉 테이블이 없다)
  • ALGORITHM = TEMPTABLE (임시 테이블 사용은 항상 뷰를 업데이트 불가능 한 것으로 만든다)

삽입이 가능하게 끔 하는 것과 관련해서는 (INSERT명령문을 가지고 업데이트가 될 수 있도록), 업데이트가 가능한 뷰는 뷰 컬럼에 대해 아래의 부가적인 요구 사항이 만족된다면 삽입 가능해(insertable) 진다.

  • 뷰 컬럼 이름이 중복 사용되지 말아야 한다.
  • 뷰는 디폴트 값을 가지고 있지 않는 베이스 테이블에 있는 모든 컬럼을 가지고 있어야 한다.
  • 뷰 컬럼은 단순 컬럼 참조(simple column reference)만 하고 컬럼을 가져오지는(derived column) 않는다. 획득하는 컬럼(derived column)은 수식에서 얻어지는 것이다. (예: 3.14159, col1 + 3, UPPER(col2), col3 / col4, (subquery))

4. The View WITH CHECK OPTION Clause

UPDATE 명령문이 LIMIT 구문을 가지고 있다면, 언더라잉 테이블에서 정의된 primary key에 대한 모든 컬럼을 뷰가 가질 수 없을 때 뷰에 대한 업데이트를 생성할 수 있는지를 결정하는 변수이다. 


5. View Metadata

SHOW CREATE VIEW 명령문을 사용하면 뷰의 메타데이터를 확인할 수 있다.


 

  • 실습

뷰 생성

 

뷰 수정(호환성 문제로 한글 사용 지양)

 

뷰 삭제

 

뷰 정보 확인

primary key 등의 정보는 확인되지 않음

 

뷰의 소스코드 확인

 

데이터 수정

 

원래 테이블에서 김병만의 birthYear가 null이면 안되기 때문에 에러 발생

 

집계함수를 포함한 뷰 생성

 

IS_UPDATABLE이 NO로 설정되어 있음

 

키 177 이상인 사람들의 뷰 생성

 

해당되는 데이터가 없음

 

실행은 되어도 뷰의 조건에 맞지 않으므로 뷰에서는 보이지 않음

 

WITH CHECK OPTION

뷰 조건에 맞지 않으므로 김병만 에러 발생

 

JOIN 된 테이블의 뷰는 업데이트 불가능

'MySQL' 카테고리의 다른 글

MySQL 전체 텍스트 검색  (0) 2020.03.19
MySQL 잠금기능  (0) 2020.02.20
MySQL SELECT문  (1) 2020.02.10

+ Recent posts