Multi Tenancy
                
              [Spring Boot] 스키마 기반 멀티테넌시 구현 (1/2)
                유저인사이트 박태양
                 2022. 8. 9. 14:50
              
              
                    
        728x90
    
    
  H2 데이터베이스를 활용하여, 스키마 기반으로 멀티테넌시를 구현하는 방법을 알아보도록 하겠습니다.
(스프링 부트 버전은 2.6.10 입니다.)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>우선 pom.xml을 위와 같이 설정해줍니다. (Flyway라는 마이그레이션 도구도 사용할 예정입니다.)
@Entity
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String username;
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;
    // getters, setters and overriden methods from UserDetails
}@Entity
public class Note {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String text;
    
    // getters and setters
}엔티티 클래스를 생성합니다.
두 클래스 모두 롬복 플러그인을 이용하여 @Getter @Setter 메서드를 생성해 주시고,
User 클래스의 경우 UserDetails의 메서드를 오버라이딩 해주세요.
getPassword() 와 같은 메서드가 클래스의 비밀번호를 반환하도록 설정해 주시고,
isAccountNonExpired()와 같은 메서드는 모두 true로 설정해주세요.
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
    Optional<User> findByUsername(String username);
}@Repository
public interface NoteRepository extends CrudRepository<Note, Long> {
}엔티티 CRUD를 위한 레파지토리를 생성합니다.
@Service
public class UserService implements UserDetailsService {
  private UserRepository repository;
  private PasswordEncoder encoder;
  private TenantService tenantService;
  public UserService(UserRepository repository, TenantService tenantService) {
    this.repository = repository;
    this.tenantService = tenantService;
    this.encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }
  @Transactional
  public User createUser(User user) {
    String encodedPassword = encoder.encode(user.getPassword());
    user.setPassword(encodedPassword);
    User saved = repository.save(user);
    tenantService.initDatabase(user.getUsername());
    return saved;
  }
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return repository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("User with the specified username is not found"));
  }
}@Service
public class NoteService {
  private NoteRepository repository;
  public NoteService(NoteRepository repository) {
    this.repository = repository;
  }
  public Note createNote(Note note) {
    return repository.save(note);
  }
  public Note findNote(Long id) {
    return repository.findById(id).orElseThrow();
  }
  public Iterable<Note> findAllNotes() {
    return repository.findAll();
  }
}엔티티 별 서비스도 구현해줍니다.
@RestController
@RequestMapping("/users")
public class UserController {
    private UserService userService;
    public UserController(UserService userService) {
        this.userService = userService;
    }
    @PostMapping
    public ResponseEntity<User> register(@RequestBody User user) {
        User created = userService.createUser(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
}@RestController
@RequestMapping("/notes")
public class NoteController {
    private NoteService noteService;
    public NoteController(NoteService noteService) {
        this.noteService = noteService;
    }
    @PostMapping
    public ResponseEntity<Note> createNote(@RequestBody Note note) {
        Note created = noteService.createNote(note);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
    @GetMapping("/{id}")
    public ResponseEntity<Note> getNote(@PathVariable Long id) {
        Note note = noteService.findNote(id);
        return ResponseEntity.ok(note);
    }
    @GetMapping
    public ResponseEntity<Iterable<Note>> getAllNotes() {
        Iterable<Note> notes = noteService.findAllNotes();
        return ResponseEntity.ok(notes);
    }
}API 호출을 위한 컨트롤러입니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private UserDetailsService userService;
    public SecurityConfig(UserDetailsService userService) {
        this.userService = userService;
    }
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST, "/users").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable()
                .headers().frameOptions().disable();
    }
}WebSecurityConfigurerAdapter가 Deprecated된걸로 알고있는데 이 부분은 다른 포스팅을 통해 다루도록 하겠습니다.
우선 프로젝트 기본 세팅을 마쳤구요, 다음 포스팅에 멀티테넌시 기능 구현 및 테스트 업로드하도록 하겠습니다.
ref : https://sultanov.dev/blog/schema-based-multi-tenancy-with-spring-data/
728x90