[컴] Remix 의 Hello World

 

Remix 의 Hello World

Helloworld

  1. npm init -y
  2. npm i @remix-run/node @remix-run/react @remix-run/serve isbot@4 react react-dom
  3. npm i -D @remix-run/dev vite
  4. vite.config.js 생성
// from : https://remix.run/docs/en/main/start/quickstart#vite-config
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remix()],
});
  1. app/root.jsx생성
  2. npx remix vite:build : <proj_root>\build에 client, server 가 만들어짐.
  3. npx remix-serve build/server/index.js : package.jsontype: module 이 필요.

package.json

{
  "name": "skybridge",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "build": "remix vite:build",
    "serve": "remix-serve build/server/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@remix-run/node": "^2.9.2",
    "@remix-run/react": "^2.9.2",
    "@remix-run/serve": "^2.9.2",
    "isbot": "^4.4.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@remix-run/dev": "^2.9.2",
    "vite": "^5.3.1"
  }
} 

모든 route 은 links function 을 export 할 수 있다.

다음과 같은 것으로 보면 된다. <link rel="stylesheet" href="./app.css?url">

import type { LinksFunction } from "@remix-run/node";

import appStylesHref from "./app.css?url";
export const links: LinksFunction = () => [
  { rel: "stylesheet", href: appStylesHref },
];

route 추가

만약 다음과 같은 file 을 만든다면, 브라우저에서 /contacts/<id> 로 갈 때 contact.$contactId.tsx 파일을 호출할 것이다.

  • /app/routes/contact.$contactId.tsx

templates, stacks

templates, stacks 를 이용하면 쉽게 초기 세팅을 끝낼 수 있다.

template 사용

quick start 에서 처럼 한번 해본 이후엔 그냥 template을 쓰면 된다.

npx create-remix@latest <my_app_name> –template remix-run/remix/templates/remix

entry.{client|server}.tsx

이 파일은 optional 이다. 이 파일이 있다면, 먼저 browser 는 entry.client.tsx를 호출하고, 그 후 /app/routes/_index.tsx를 실행한다.

Layout

root.tsxLayout function 이 있으면 그것이 기본적인 Layout 으로 사용된다.

_index.tsx

보통 아래처럼 default function 을 지정하는데, Index 는 다른 이름으로 바꿔도 문제되지 않는다.

...
export default function Index() {
    return (
        <div></div>
    )
}

접근 순서

  1. entry.client.tsx
  2. app/root.tsxLayout
  3. app/root.tsxApp
  4. app/routes/_index.tsx

See Also

  1. Query GraphQL in Remix in less than 60 seconds | simeonGriggs.dev : graphql 사용

[컴] ionic 으로 android apk 생성

cross platform / cordova

ionic 으로 android apk 생성

ionic start

  • Examples
  • ionic start --list : template list 를 볼 수 있다.
  • ionic start myApp tabs --type=react : myApp 이란 이름으로, tabs 라는 template을 사용해서 project생성
npm install -g @ionic/cli

ionic start --list
ionic start myApp tabs --type=react 

ionic capacitor

ionic 은 기본적으로 Capacitor 를 사용한다.(Capacitor: Everything You've Ever Wanted to Know - Ionic Blog)

  • ionic serve : browser에서 app 을 보여준다.
  • ionic capacitor add android : Capacitor 를 이용해서 android app 추가
  • ionic capacitor build android : ionic capacitor build | Ionic Documentation
  • ionic capacitor open android : android project를 android studio 에서 열어준다.

emulator

emulator 를 띄워놓고, ionic capacitor run android --target=Pixel_3a_API_34 를 하면, emulator 에 apk 가 설치되고 실행된다.

  1. 환경변수 설정

    • JAVA_HOME은 android build 시점에 필요하다.
    set JAVA_HOME=d:\a\apps\java\jdk-17.0.11.9-hotspot
    set ANDROID_HOME=d:\a\appss\Android\Sdk
  2. emulator list : \Android\Sdk\emulator\emulator.exe -list-avd ionic capacitor run android --list 을 사용할 수도 있다. bat ionic capacitor run android --list

  3. run emulator: \Android\Sdk\emulator\emulator.exe -avd Pixel_3a_API_34

  4. ionic capacitor run android --traget=<target>

build .apk

ionic capacitor open android 를 실행하면, <proj_root>\android 를 android studio 에서 열어준다. android studio 에서 ‘build apk’ 를 하면 apk 를 얻을 수 있다.

  • apk path: <proj_root>\android\app\build\outputs\apk\debug\app-debug.apk

See Also

  1. 쿠…sal: [컴] cordova 로 android app build
  2. Starting an App: How to Guide | Ionic Documentation

[컴] cordova 로 android app build

 apache cordova / cross platform /

cordova 로 android app build

절차

  1. npm 설치
  2. npm i --save cordova
  3. cordova create mytestapp com.mysite.myapp MyTestApp --template https://github.com/apache/cordova-app-hello-world
  4. cd mytestapp
  5. cordova platform ls
  6. cordova platform add android
  7. cordova build android

cordova create

  • cordova create <path> <id> <name>
  • template 사용:
    • App Templates - Apache Cordova
    • cordova-template-ngx-onsenui
    • 예 : cordova.cmd create mytestapp com.mysite.myapp MyTestApp --template cordova-template-ngx-onsenui

cordova build

prerequisites

  • gradle : Gradle | Releases
  • java 17 이상
  • android sdk : android studio 설치후, sdk manager 로 download 하면 된다.

cordova build android

set PATH=d:\a\apps\gradle\gradle-7.5.1\bin;%path%
set JAVA_HOME=d:\a\apps\java\jdk-17.0.11.9-hotspot
set ANDROID_HOME=d:\a\appss\Android\Sdk
d:\a\prog\cordova\node_modules\.bin\cordova.cmd build android

Error, Could not get resource

테스트를 한 환경의 wifi 가 stable 하지 않았는데, 그래서 다음처럼 간혹 Could not get resource 에러가 떴다. 이럴 때는 그냥 다시 build 를 시도하면 된다. 그러면, download 가 잘 이뤄져서 이 이슈는 넘어간다. 이런 식으로 Could not get resource 가 뜨는 경우는 계속 재시도 하면 된다.

* What went wrong:
Execution failed for task ':CordovaLib:compileDebugJavaWithJavac'.
> Could not resolve all files for configuration ':CordovaLib:debugCompileClasspath'.
   > Failed to transform kotlin-stdlib-1.7.10.jar (org.jetbrains.kotlin:kotlin-stdlib:1.7.10) to match attributes {artifactType=android-classes-jar, org.gradle.category=library, org.gradle.libraryelements=jar, org.gradle.status=release, org.gradle.usage=java-api}.
      > Could not download kotlin-stdlib-1.7.10.jar (org.jetbrains.kotlin:kotlin-stdlib:1.7.10)
         > Could not get resource 'https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.7.10/kotlin-stdlib-1.7.10.jar'.
            > Tag mismatch

.apk

다음 path 에서 결과물을 확인할 수 있다.

  • <proj_root>\platforms\android\app\build\outputs\apk\debug\app-debug.apk

emulator

Android Virtual Device(AVD) 에서 cordova 로 만든 apk 를 실행할 수 있다.

일단 먼저 AVD 를 띄워놓고, cordova run --emulator 를 실행하면 된다.

avd 실행

android studio 에 가서 avd manager 에서 띄워도 되고, emulator 실행파일을 이용해서 띄워도 된다.

Reference

  1. Android Platform Guide - Apache Cordova
  2. 쿠…sal: [컴] cordova 로 web app 만들기
  3. Apache Cordova

[컴] spring에서 @Transactional 이 동작하는지 확인하는 방법

spring transaction / transactional / 스프링 / spring boot

spring에서 @Transactional 이 동작하는지 확인하는 방법

@Transactional 이 제대로 동작하려면

spring 에서 @Transactional 를 사용하려면, public method 에 annotation 을 달아야 한다. 정확히는 이 @Transactional이 붙어있는 method 를 현재 @Bean 말고, 다른 Bean 에서 호출해줘야 한다.

  • [Using @Transactional :: Spring Framework](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html)
  • [java - Spring @Transactional annotation is not working - Stack Overflow](https://stackoverflow.com/questions/32156652/spring-transactional-annotation-is-not-working)

@Transactional 동작하는지 확인하는 방법

  • TransactionAspectSupport.currentTransactionStatus
    import org.springframework.transaction.interceptor.TransactionAspectSupport;
    ...
    status = TransactionAspectSupport.currentTransactionStatus();
    • Transaction 이 동작하지 않았다면, NoTransactionException 을 던지게 된다.
  • call stack
    • TransactionInterceptor.invoke : call stack 에 TransactionInterceptor.invoke가 보인다.
    • call stack

@Transactional method 를 다른 Bean 에서 호출해야 하는 이유

다른 Bean 에서 호출하지 않고, 현재 @Bean 내의 method 에서 @Transactional method 를 호출하는 것으로는 bean 을 생성하지 않는다. 그러니까 spring 이 처음 run 할 때 @Transactional에 대한 proxy class 를 만드는데, 그 작업을 하지 않는 것 같다.

cglib 를 호출할 때의 call stack:

- SpringNamingPolicy.getClassName(String,String,Object,Predicate) (\spring-core-6.0.7.jar\org.springframework.cglib.core\SpringNamingPolicy.class:41)
- AbstractClassGenerator.generateClassName(Predicate) (\spring-core-6.0.7.jar\org.springframework.cglib.core\AbstractClassGenerator.class:169)
- AbstractClassGenerator.generate(AbstractClassGenerator$ClassLoaderData) (\spring-core-6.0.7.jar\org.springframework.cglib.core\AbstractClassGenerator.class:339)
- Enhancer.generate(AbstractClassGenerator$ClassLoaderData) (\spring-core-6.0.7.jar\org.springframework.cglib.proxy\Enhancer.class:575)
- AbstractClassGenerator$ClassLoaderData.lambda$new$1(AbstractClassGenerator) (\spring-core-6.0.7.jar\org.springframework.cglib.core\AbstractClassGenerator.class:103)
- 0x0000000800f7bf70.apply(Object) (Unknown Source:-1)
- LoadingCache.lambda$createEntry$1(Object) (\spring-core-6.0.7.jar\org.springframework.cglib.core.internal\LoadingCache.class:52)
- 0x0000000800f7c5c0.call() (Unknown Source:-1)
- FutureTask.run() (\java.base\java.util.concurrent\FutureTask.class:264)
- LoadingCache.createEntry(Object,Object,Object) (\spring-core-6.0.7.jar\org.springframework.cglib.core.internal\LoadingCache.class:57)
- LoadingCache.get(Object) (\spring-core-6.0.7.jar\org.springframework.cglib.core.internal\LoadingCache.class:34)
- AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator,boolean) (\spring-core-6.0.7.jar\org.springframework.cglib.core\AbstractClassGenerator.class:126)
- AbstractClassGenerator.create(Object) (\spring-core-6.0.7.jar\org.springframework.cglib.core\AbstractClassGenerator.class:313)
- Enhancer.createHelper() (\spring-core-6.0.7.jar\org.springframework.cglib.proxy\Enhancer.class:562)
- Enhancer.createClass() (\spring-core-6.0.7.jar\org.springframework.cglib.proxy\Enhancer.class:407)
- ConfigurationClassEnhancer.createClass(Enhancer) (\spring-context-6.0.7.jar\org.springframework.context.annotation\ConfigurationClassEnhancer.class:138)
- ConfigurationClassEnhancer.enhance(Class,ClassLoader) (\spring-context-6.0.7.jar\org.springframework.context.annotation\ConfigurationClassEnhancer.class:109)
- ConfigurationClassPostProcessor.enhanceConfigurationClasses(ConfigurableListableBeanFactory) (\spring-context-6.0.7.jar\org.springframework.context.annotation\ConfigurationClassPostProcessor.class:514)
- ConfigurationClassPostProcessor.postProcessBeanFactory(ConfigurableListableBeanFactory) (\spring-context-6.0.7.jar\org.springframework.context.annotation\ConfigurationClassPostProcessor.class:304)
- PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(Collection,ConfigurableListableBeanFactory) (\spring-context-6.0.7.jar\org.springframework.context.support\PostProcessorRegistrationDelegate.class:358)
- PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory,List) (\spring-context-6.0.7.jar\org.springframework.context.support\PostProcessorRegistrationDelegate.class:150)
- AbstractApplicationContext.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory) (\spring-context-6.0.7.jar\org.springframework.context.support\AbstractApplicationContext.class:747)
- AbstractApplicationContext.refresh() (\spring-context-6.0.7.jar\org.springframework.context.support\AbstractApplicationContext.class:565)
- ServletWebServerApplicationContext.refresh() (\spring-boot-3.0.5.jar\org.springframework.boot.web.servlet.context\ServletWebServerApplicationContext.class:146)
- SpringApplication.refresh(ConfigurableApplicationContext) (\spring-boot-3.0.5.jar\org.springframework.boot\SpringApplication.class:732)
- SpringApplication.refreshContext(ConfigurableApplicationContext) (\spring-boot-3.0.5.jar\org.springframework.boot\SpringApplication.class:434)
- SpringApplication.run(String[]) (\spring-boot-3.0.5.jar\org.springframework.boot\SpringApplication.class:310)
- FishApplication.main(String[]) (d:\a\prog\foodpang\purchase\fish\fish\src\main\java\co\foodpang\fish\FishApplication.java:24)
- NativeMethodAccessorImpl.invoke0(Method,Object,Object[])[native method] (\java.base\jdk.internal.reflect\NativeMethodAccessorImpl.class:-1)
- NativeMethodAccessorImpl.invoke(Object,Object[]) (\java.base\jdk.internal.reflect\NativeMethodAccessorImpl.class:77)
- DelegatingMethodAccessorImpl.invoke(Object,Object[]) (\java.base\jdk.internal.reflect\DelegatingMethodAccessorImpl.class:43)
- Method.invoke(Object,Object[]) (\java.base\java.lang.reflect\Method.class:568)
- RestartLauncher.run() (\spring-boot-devtools-3.0.5.jar\org.springframework.boot.devtools.restart\RestartLauncher.class:49)

[컴] JPA 의 entity 로 foreign key 설정하는 법

외래키 / hibernate / join / spring boot / spring / java / orm / 연결 / 관계 설정 / relation entity

JPA 의 entity 로 foreign key 설정하는 법

user table 의 profile_id 가 profile table 의 id 를 참조하도록 하는 예시이다.

아래 code example 이 있다.

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;

@Entity
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "profile_id", referencedColumnName = "id", nullable=false)
    private Profile profile;

    ...
}
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;

import java.time.LocalDateTime;

@Entity
public class Profile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @OneToOne(mappedBy = "profile")
    private User user;

create table user (id bigint not null auto_increment, profile_id bigint not null, primary key (id)) engine=InnoDB;
create table profile (id bigint not null, primary key (id)) engine=InnoDB;
alter table user add constraint FKl6pykqur53o90qdnhjko0r8ni foreign key (profile_id) references profile (id);

어느 값이 어느값을 가리키는지는 아래 그림을 참고하자.

연결

See Also

  1. Spring Data JPA One To One Relationship Mapping Example
  2. java - When Should I Use @JoinColumn or @JoinTable with JPA? - Stack Overflow

[컴] CQRS pattern

 

CQRS pattern

CQRS (Command Query Responsibility Segregation) 의 핵심은 read/write 가 구분된다는 점이다.

보통 우리가 client 라고 부르는 쪽에서 필요한 데이터를 가져올때(read) 와 변경사항을 저장할 때(write) 우리는 같은 model 을 사용한다.

그런데 CQRS 는 이것을 각자 다른 model 을 사용한다. 한 예는 read때는 read.myserver.com 을 사용하고, write 때는 write.myserver.com 을 사용한다고 봐도 된다.

간략하게 어떤 개념인지를 파악하는데는 마틴파울러의 글이 좋은 듯 싶다. 글에 있는 이미지도 이해를 하는데 도움이 된다.

  • 마틴파울러, CQRS
    • CQRS 의 사용을 주의하라고 한다. 이것은 복잡성을 증가시킬 요인이 크다고 이야기 한다.
    • 시스템의 일부에서만 사용하라고 이야기한다.

CQRS pattern - Azure Architecture Center | Microsoft Learn

CQRS pattern - Azure Architecture Center | Microsoft Learn 의 내용을 일부정리

CQRS 에선 command 는 update 할 때 query 를 read 할 때 쓰인다. 그리고 write 과 read 에 대해서 각자 다른 model들을 사용한다.

  • data 를 update 할 때는 command

  • data 를 read 할 때는 query

  • command 는 task based 여야 한다. data centric 이 아니라.

    • 예를 들어, ’호텔객실 예약’이라고 해야지, ’ReservationStatus 를 Reserved 로 변경’이라고 하지 않는다.
    • 한가지 방법은 command를 보내기 전에,
    • client에서 validation rule들 실행하고,
    • 버튼을 비활성화하고,
    • UI 에 이유를 설명하는 것
    • 예를 들면, ‘방이 남지 않았습니다.’
    • 이 방법으로 server 쪽 command 의 실패의 원인은 race condition 들(2개의 유저들이 마지막 방을 예약하려고 하는것)로 좁혀질 수 있다. 심지어 이런 때도 데이터와 로직(손님을 대기목록에 넣는 것)을 추가해서 해결할 수 있다.
  • command들은 synchronous 하게 처리되지 않고, queue 에 넣어서 asynchronous 하게 처리할 수 있다.

  • Query 들은 db를 변경하지 않는다. query는 도메인 지식을 캡슐화하지 않는 DTO를 반환한다.

이렇게 write 와 read 에 대한 model 을 분리하면, 설계(design), 구현(implementation)이 간단해진다.

isolation 을 강화하기 위해 물리적으로 read용, write용으로 db를 나눠도 된다. read 의 경우 필요한 join 등을 미리 해놓은 결과를 저장해서 보여주는 것처럼 다른 schema 를 사용할 수 있다. 이것은 materialized view 를 쓰거나, NoSQL 같은 다른 type 의 db 를 쓰는 등의 방법을 사용할 수 있다.

이 경우 write 에 사용하는 db 와 read 에 사용하는 db 의 동기화가 필요한데, 이때는 대체로 write db 에 update 가 발생할 때 event 를 발생시켜서 read db 를 update 하게 된다. 다만 이때의 문제는 db에 write 가 된 이후에 event 가 성공적으로 전달되지 못하는 경우 data 의 consistency 를 보장할 수 없다.

read store 와 write store read store 는 write store 의 복제본(read-only replica)이거나, write store 와 상관없이 다른 구조를 갖고 있을 수도 있다. - 여러개의 read-only replica 를 갖고 있으면, 이 replica 가 application instance 에 가까이 있는 경우, query 성능이 향상 될 것이다. - 보통 read store 의 부하가 더 높은데, read 와 write store 를 분리하면, 각각의 부하에 맞게 적절하게 확장이 가능하다.

CQRS 일부 구현은 event sourcing pattern 을 사용한다.

  • application 의 state 가 sequence of events 로 저장된다.
  • 각 event 는 data에 대한 set of changes 를 갖는다.
  • 그래서 이 event 들을 다시 실행하면, 현재의 state 가 되는 것이다.
  • event sourcing pattern 을 사용하면 design 이 복잡해 진다.

구현상 어려운점

  • 복잡성, 개념은 간단한데, application design 이 복잡하다. event sourcing pattern 이 들어가면 더 복잡하다.
  • 메시징, application 은 message 의 실패, duplicated message 에 대한 처리를 해야 한다.
  • Eventual consistency(최종 일관성) : read db 의 data 가 최신이 아닐 수 있다.(stale) 이렇게 stale data 를 기반으로 write 을 요청하는 경우도 있을 수 있는데, 그것을 구분하기 어렵다.

언제 CQRS pattern 을 쓰는가?

  • 많은 사용자가 동일한 데이터에 동시에 액세스 하는
  • 복잡한 프로세스를 안내하는 task-based UI 에서 쓸 수 있다.
  • 쓰기 model 보다 read model 의 성능이 더 중요한 경우, write에 비해 더 많은 read 가 있는 경우등에 유리, write model instance 가 적으면, merge conflict 발생을 최소화할 수 있다.
  • 한팀은 복잡한 domain model 과 write model 에 집중하고, 다른 한팀은 read model 과 UI 에 집중할 수 있는 상황이라면 사용할 수 있다.(me: 세부적인 내용은 다르겠지만, server side, client side 로 나눠서 개발하는 것과 비슷한 경우일듯)
  • 시간이 지남에 따라 진화하고, 여러 다양한 버전의 model 을 포함할 수도 있는 시스템
  • 비지니스 규칙이 정기적으로 변화하하는 시나리오
  • 다른 시스템들과의 통합, 특히 event sourcing 과 결합된 경우.
    • 하나의 sub system 의 일시적인 오류가 다른 sub system 으로 영향이 미치지 않아야 하는 시스템

쓰지 않는게 나은 경우

  • domain 이나 business logic 이 간단한 경우
  • 간단한 CRUD style 이면 충분한 경우

CQRS pattern 을 시스템의 제한된 부분(이 부분이 추후 중요해질 것이라 여겨지는)에 적용하는 것을 고려해보자.

예시들 best practices

[컴] 포토샵으로 변형된 얼굴사진 감별

 

얼굴 포토샵 감지 / 감별 / 확인 방법 / 뽀샵

포토샵으로 변형된 얼굴사진 감별

FAL detector, Face-Aware Liquify detector

Adobe Research와 UC Berkley에서 개발한 AI 기술인 FAL Detector는 포토샵의 Face-Aware Liquify’ 기능을 이용하여 왜곡된 이미지를 학습

Reference

  1. 유명인 얼굴이 어떻게 포토샵 처리되었는지 보여주는 AI 도구 등장 : 클리앙
  2. AI Tool Reveals How Celebrities’ Faces Have Been Photoshopped | PetaPixel